@kevludwig/eve-lexware-office 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/extension/_manifest.json +14 -0
- package/dist/extension/extension.d.ts +36 -0
- package/dist/extension/extension.mjs +36 -0
- package/dist/extension/hooks/approval-cards.d.ts +8 -0
- package/dist/extension/hooks/approval-cards.mjs +26 -0
- package/dist/extension/hooks/declined-reminders.d.ts +7 -0
- package/dist/extension/hooks/declined-reminders.mjs +27 -0
- package/dist/extension/instructions.md +16 -0
- package/dist/extension/lib/cards.d.ts +8 -0
- package/dist/extension/lib/cards.mjs +156 -0
- package/dist/extension/lib/findings.d.ts +128 -0
- package/dist/extension/lib/findings.mjs +176 -0
- package/dist/extension/lib/format.d.ts +3 -0
- package/dist/extension/lib/format.mjs +16 -0
- package/dist/extension/lib/journal.d.ts +15 -0
- package/dist/extension/lib/journal.mjs +27 -0
- package/dist/extension/lib/purchase-card.d.ts +3 -0
- package/dist/extension/lib/purchase-card.mjs +66 -0
- package/dist/extension/lib/purchase.d.ts +112 -0
- package/dist/extension/lib/purchase.mjs +346 -0
- package/dist/extension/lib/runtime.d.ts +18 -0
- package/dist/extension/lib/runtime.mjs +47 -0
- package/dist/extension/lib/sales.d.ts +72 -0
- package/dist/extension/lib/sales.mjs +292 -0
- package/dist/extension/lib/state.d.ts +9 -0
- package/dist/extension/lib/state.mjs +44 -0
- package/dist/extension/lib/types.d.ts +43 -0
- package/dist/extension/lib/types.mjs +7 -0
- package/dist/extension/skills/eingangsrechnung/SKILL.md +124 -0
- package/dist/extension/skills/verkaufsbelege/SKILL.md +94 -0
- package/dist/extension/skills/zahlungserinnerung/SKILL.md +37 -0
- package/dist/extension/tools/create_customer.d.ts +44 -0
- package/dist/extension/tools/create_customer.mjs +107 -0
- package/dist/extension/tools/create_invoice.d.ts +28 -0
- package/dist/extension/tools/create_invoice.mjs +28 -0
- package/dist/extension/tools/create_order_confirmation.d.ts +28 -0
- package/dist/extension/tools/create_order_confirmation.mjs +33 -0
- package/dist/extension/tools/create_purchase_invoice.d.ts +76 -0
- package/dist/extension/tools/create_purchase_invoice.mjs +241 -0
- package/dist/extension/tools/create_quotation.d.ts +28 -0
- package/dist/extension/tools/create_quotation.mjs +28 -0
- package/dist/extension/tools/due_payment_reminders.d.ts +39 -0
- package/dist/extension/tools/due_payment_reminders.mjs +56 -0
- package/dist/extension/tools/read.d.ts +10 -0
- package/dist/extension/tools/read.mjs +83 -0
- package/dist/extension/tools/send_payment_reminder.d.ts +57 -0
- package/dist/extension/tools/send_payment_reminder.mjs +180 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +8 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.mjs +15 -0
- package/package.json +69 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
declare const _default: import("eve/tools").ToolDefinition<{
|
|
2
|
+
company_name?: string | undefined;
|
|
3
|
+
first_name?: string | undefined;
|
|
4
|
+
last_name?: string | undefined;
|
|
5
|
+
street: string;
|
|
6
|
+
zip: string;
|
|
7
|
+
city: string;
|
|
8
|
+
country_code?: string | undefined;
|
|
9
|
+
email?: string | undefined;
|
|
10
|
+
phone?: string | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
contactId: string;
|
|
13
|
+
note: string;
|
|
14
|
+
customerNumber?: undefined;
|
|
15
|
+
name?: undefined;
|
|
16
|
+
} | {
|
|
17
|
+
contactId: string;
|
|
18
|
+
customerNumber: number | undefined;
|
|
19
|
+
name: string;
|
|
20
|
+
note: string | undefined;
|
|
21
|
+
}> & {
|
|
22
|
+
execute(input: {
|
|
23
|
+
company_name?: string | undefined;
|
|
24
|
+
first_name?: string | undefined;
|
|
25
|
+
last_name?: string | undefined;
|
|
26
|
+
street: string;
|
|
27
|
+
zip: string;
|
|
28
|
+
city: string;
|
|
29
|
+
country_code?: string | undefined;
|
|
30
|
+
email?: string | undefined;
|
|
31
|
+
phone?: string | undefined;
|
|
32
|
+
}, ctx: import("eve/tools").ToolContext): Promise<{
|
|
33
|
+
contactId: string;
|
|
34
|
+
note: string;
|
|
35
|
+
customerNumber?: undefined;
|
|
36
|
+
name?: undefined;
|
|
37
|
+
} | {
|
|
38
|
+
contactId: string;
|
|
39
|
+
customerNumber: number | undefined;
|
|
40
|
+
name: string;
|
|
41
|
+
note: string | undefined;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
export default _default;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { approverPolicy, client } from "../lib/runtime.mjs";
|
|
8
|
+
import { saveWarnings } from "../lib/findings.mjs";
|
|
9
|
+
import { readJournal, writeJournal } from "../lib/journal.mjs";
|
|
10
|
+
import { rememberShownContacts, shownContactsOf } from "../lib/state.mjs";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { createCustomer, describeError, findContactsByName } from "@kevludwig/lexware-office";
|
|
13
|
+
import { defineTool } from "eve/tools";
|
|
14
|
+
const inputSchema = z.object({
|
|
15
|
+
company_name: z.string().trim().min(1).max(255).optional().describe("Firmenname inkl. Rechtsform — entweder dieser oder last_name"),
|
|
16
|
+
first_name: z.string().trim().max(100).optional().describe("Vorname (nur bei Privatperson)"),
|
|
17
|
+
last_name: z.string().trim().min(1).max(100).optional().describe("Nachname (nur bei Privatperson) — entweder dieser oder company_name"),
|
|
18
|
+
street: z.string().trim().min(1).max(200).describe("Straße und Hausnummer"),
|
|
19
|
+
zip: z.string().trim().min(1).max(10).describe("Postleitzahl"),
|
|
20
|
+
city: z.string().trim().min(1).max(100).describe("Ort"),
|
|
21
|
+
country_code: z.string().trim().length(2).optional().describe("ISO-Ländercode, Standard DE"),
|
|
22
|
+
email: z.string().trim().email().max(200).optional().describe("E-Mail-Adresse (optional)"),
|
|
23
|
+
phone: z.string().trim().max(50).optional().describe("Telefonnummer (optional)")
|
|
24
|
+
}).strict().refine((input) => Boolean(input.company_name) !== Boolean(input.last_name), { message: "Genau eines von company_name und last_name angeben." });
|
|
25
|
+
const nameOf = (input) => input.company_name?.trim() || input.last_name?.trim() || "";
|
|
26
|
+
const label = (contact) => `${contact.name}${contact.number ? ` (Nr. ${contact.number})` : ""}`;
|
|
27
|
+
var create_customer_default = defineTool({
|
|
28
|
+
description: "Legt einen neuen Kunden in Lexware Office an. Pflicht sind Name (Firma oder Person) UND die Rechnungsadresse (Straße, PLZ, Ort) — fehlt die Adresse, frage sie zuerst beim Nutzer ab. E-Mail und Telefon sind optional. Prüfe vorher mit dem Lese-Tool (/contacts?name=…), ob der Kunde schon existiert. Legt erst nach Freigabe an.",
|
|
29
|
+
inputSchema,
|
|
30
|
+
approval: {
|
|
31
|
+
request: async (ctx) => {
|
|
32
|
+
const name = nameOf(ctx.toolInput ?? {});
|
|
33
|
+
if (!name) return "user-approval";
|
|
34
|
+
const findings = [];
|
|
35
|
+
try {
|
|
36
|
+
const similar = await findContactsByName(client(), name, {
|
|
37
|
+
size: 5,
|
|
38
|
+
signal: ctx.abortSignal
|
|
39
|
+
});
|
|
40
|
+
if (similar.length > 0) findings.push({
|
|
41
|
+
kind: "note",
|
|
42
|
+
title: "⚠ Ähnliche Kontakte",
|
|
43
|
+
value: similar.map(label).join(", ")
|
|
44
|
+
});
|
|
45
|
+
rememberShownContacts(ctx.callId, similar.map((contact) => contact.id));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
findings.push({
|
|
48
|
+
kind: "note",
|
|
49
|
+
title: "⚠ Suche fehlgeschlagen",
|
|
50
|
+
value: `Ähnliche Kontakte ungeprüft: ${describeError(error)}`
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
saveWarnings(ctx.callId, findings);
|
|
54
|
+
return "user-approval";
|
|
55
|
+
},
|
|
56
|
+
response: approverPolicy
|
|
57
|
+
},
|
|
58
|
+
async execute(input, ctx) {
|
|
59
|
+
const journal = await readJournal("customer", ctx.callId);
|
|
60
|
+
if (journal?.resourceId) return {
|
|
61
|
+
contactId: journal.resourceId,
|
|
62
|
+
note: "Wiederaufnahme: Der Kunde war bereits angelegt — nichts doppelt erstellt."
|
|
63
|
+
};
|
|
64
|
+
const shown = shownContactsOf(ctx.callId);
|
|
65
|
+
try {
|
|
66
|
+
const unseen = (await findContactsByName(client(), nameOf(input), {
|
|
67
|
+
size: 5,
|
|
68
|
+
signal: ctx.abortSignal
|
|
69
|
+
})).filter((contact) => !shown.has(contact.id));
|
|
70
|
+
if (unseen.length > 0) throw new Error(`Abgebrochen: Inzwischen gibt es einen ähnlichen Kontakt, den die Karte nicht zeigte: ${unseen.map(label).join(", ")}. Nichts angelegt. Rufe das Tool erneut auf, wenn trotzdem ein neuer Kunde entstehen soll.`);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof Error && error.message.startsWith("Abgebrochen")) throw error;
|
|
73
|
+
}
|
|
74
|
+
await writeJournal("customer", ctx.callId, { status: "pending" });
|
|
75
|
+
let created;
|
|
76
|
+
try {
|
|
77
|
+
created = await createCustomer(client(), {
|
|
78
|
+
...input.company_name ? { company: { name: input.company_name } } : {},
|
|
79
|
+
...input.last_name ? { person: {
|
|
80
|
+
...input.first_name ? { firstName: input.first_name } : {},
|
|
81
|
+
lastName: input.last_name
|
|
82
|
+
} } : {},
|
|
83
|
+
billingAddress: {
|
|
84
|
+
street: input.street,
|
|
85
|
+
zip: input.zip,
|
|
86
|
+
city: input.city,
|
|
87
|
+
countryCode: input.country_code
|
|
88
|
+
},
|
|
89
|
+
email: input.email,
|
|
90
|
+
phone: input.phone
|
|
91
|
+
}, ctx.abortSignal);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(`Kunde nicht angelegt: ${describeError(error)}`);
|
|
94
|
+
}
|
|
95
|
+
await writeJournal("customer", ctx.callId, {
|
|
96
|
+
status: "created",
|
|
97
|
+
resourceId: created.id
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
contactId: created.id,
|
|
101
|
+
customerNumber: created.customerNumber,
|
|
102
|
+
name: created.name,
|
|
103
|
+
note: created.customerNumber === void 0 ? "Angelegt; die Kundennummer ließ sich nicht zurücklesen." : void 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
export { create_customer_default as default };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
declare const _default: import("eve/tools").ToolDefinition<{
|
|
2
|
+
customer_number?: number | undefined;
|
|
3
|
+
contact_id?: string | undefined;
|
|
4
|
+
items: {
|
|
5
|
+
name: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
unit: string;
|
|
8
|
+
net_price: number;
|
|
9
|
+
tax_rate: 0 | 7 | 19;
|
|
10
|
+
}[];
|
|
11
|
+
source_document_id?: string | undefined;
|
|
12
|
+
source_document_type?: "order-confirmation" | "quotation" | undefined;
|
|
13
|
+
}, Record<string, unknown>> & {
|
|
14
|
+
execute(input: {
|
|
15
|
+
customer_number?: number | undefined;
|
|
16
|
+
contact_id?: string | undefined;
|
|
17
|
+
items: {
|
|
18
|
+
name: string;
|
|
19
|
+
quantity: number;
|
|
20
|
+
unit: string;
|
|
21
|
+
net_price: number;
|
|
22
|
+
tax_rate: 0 | 7 | 19;
|
|
23
|
+
}[];
|
|
24
|
+
source_document_id?: string | undefined;
|
|
25
|
+
source_document_type?: "order-confirmation" | "quotation" | undefined;
|
|
26
|
+
}, ctx: import("eve/tools").ToolContext): Promise<Record<string, unknown>>;
|
|
27
|
+
};
|
|
28
|
+
export default _default;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { approverPolicy } from "../lib/runtime.mjs";
|
|
8
|
+
import { customerFields, executeSalesDocument, lineItemSchema, salesApproval } from "../lib/sales.mjs";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { defineTool } from "eve/tools";
|
|
11
|
+
const inputSchema = z.object({
|
|
12
|
+
...customerFields,
|
|
13
|
+
items: z.array(lineItemSchema).min(1).max(300).describe("Alle Positionen der Rechnung"),
|
|
14
|
+
source_document_id: z.string().uuid().optional().describe("UUID des Angebots bzw. der AB, aus der diese Rechnung hervorgeht"),
|
|
15
|
+
source_document_type: z.enum(["quotation", "order-confirmation"]).optional().describe("Art des Bezugsbelegs zu source_document_id")
|
|
16
|
+
}).strict().refine((input) => input.customer_number === void 0 !== (input.contact_id === void 0), { message: "Genau eines von customer_number und contact_id angeben." }).refine((input) => !input.source_document_id || input.source_document_type, { message: "source_document_type zu source_document_id angeben." });
|
|
17
|
+
var create_invoice_default = defineTool({
|
|
18
|
+
description: "Erstellt eine Ausgangsrechnung als Entwurf in Lexware Office — festgeschrieben und versendet wird dort, nie hier. Kunde per Kundennummer oder contact_id, dazu die Positionen; source_document_id, wenn die Rechnung aus einem Angebot oder einer AB hervorgeht. Legt erst nach Freigabe an und prüft vorher auf Dubletten.",
|
|
19
|
+
inputSchema,
|
|
20
|
+
approval: {
|
|
21
|
+
request: salesApproval("invoice"),
|
|
22
|
+
response: approverPolicy
|
|
23
|
+
},
|
|
24
|
+
async execute(input, ctx) {
|
|
25
|
+
return executeSalesDocument("invoice", input, ctx);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
export { create_invoice_default as default };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
declare const _default: import("eve/tools").ToolDefinition<{
|
|
2
|
+
customer_number?: number | undefined;
|
|
3
|
+
contact_id?: string | undefined;
|
|
4
|
+
items: {
|
|
5
|
+
name: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
unit: string;
|
|
8
|
+
net_price: number;
|
|
9
|
+
tax_rate: 0 | 7 | 19;
|
|
10
|
+
}[];
|
|
11
|
+
title?: string | undefined;
|
|
12
|
+
quotation_id?: string | undefined;
|
|
13
|
+
}, Record<string, unknown>> & {
|
|
14
|
+
execute(input: {
|
|
15
|
+
customer_number?: number | undefined;
|
|
16
|
+
contact_id?: string | undefined;
|
|
17
|
+
items: {
|
|
18
|
+
name: string;
|
|
19
|
+
quantity: number;
|
|
20
|
+
unit: string;
|
|
21
|
+
net_price: number;
|
|
22
|
+
tax_rate: 0 | 7 | 19;
|
|
23
|
+
}[];
|
|
24
|
+
title?: string | undefined;
|
|
25
|
+
quotation_id?: string | undefined;
|
|
26
|
+
}, ctx: import("eve/tools").ToolContext): Promise<Record<string, unknown>>;
|
|
27
|
+
};
|
|
28
|
+
export default _default;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { approverPolicy } from "../lib/runtime.mjs";
|
|
8
|
+
import { customerFields, executeSalesDocument, lineItemSchema, salesApproval, titleSchema } from "../lib/sales.mjs";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { defineTool } from "eve/tools";
|
|
11
|
+
const inputSchema = z.object({
|
|
12
|
+
...customerFields,
|
|
13
|
+
items: z.array(lineItemSchema).min(1).max(300).describe("Alle Positionen der AB — beim Ableiten aus einem Angebot dessen Positionen (per Lese-Tool /quotations/{id}), plus gewünschte Änderungen"),
|
|
14
|
+
title: titleSchema("der AB"),
|
|
15
|
+
quotation_id: z.string().uuid().optional().describe("UUID des Angebots, aus dem diese AB hervorgeht. Dann prüft das Tool Kunde und Positionssumme gegen das Angebot.")
|
|
16
|
+
}).strict().refine((input) => input.customer_number === void 0 !== (input.contact_id === void 0), { message: "Genau eines von customer_number und contact_id angeben." });
|
|
17
|
+
const withSource = (input) => input.quotation_id && !input.source_document_id ? {
|
|
18
|
+
...input,
|
|
19
|
+
source_document_id: input.quotation_id,
|
|
20
|
+
source_document_type: "quotation"
|
|
21
|
+
} : input;
|
|
22
|
+
var create_order_confirmation_default = defineTool({
|
|
23
|
+
description: "Erstellt eine Auftragsbestätigung (AB) in Lexware Office und schreibt sie direkt fest (sie erhält eine AB-Nummer; kein Entwurf). Geht die AB aus einem Angebot hervor, dessen quotation_id mitgeben — das Angebot vorher über /voucherlist (voucherType 'quotation') suchen und lesen; bei mehreren passenden den Nutzer wählen lassen. Legt erst nach Freigabe an.",
|
|
24
|
+
inputSchema,
|
|
25
|
+
approval: {
|
|
26
|
+
request: salesApproval("order-confirmation", withSource),
|
|
27
|
+
response: approverPolicy
|
|
28
|
+
},
|
|
29
|
+
async execute(input, ctx) {
|
|
30
|
+
return executeSalesDocument("order-confirmation", withSource(input), ctx);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
export { create_order_confirmation_default as default };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
declare const _default: import("eve/tools").ToolDefinition<{
|
|
2
|
+
supplier_name: string;
|
|
3
|
+
voucher_number: string;
|
|
4
|
+
voucher_date: string;
|
|
5
|
+
due_date?: string | undefined;
|
|
6
|
+
currency: string;
|
|
7
|
+
bank_statement_eur_amount?: number | undefined;
|
|
8
|
+
total_gross_amount: number;
|
|
9
|
+
tax_groups: {
|
|
10
|
+
gross_amount: number;
|
|
11
|
+
tax_rate_percent: number;
|
|
12
|
+
category?: string | undefined;
|
|
13
|
+
}[];
|
|
14
|
+
category?: string | undefined;
|
|
15
|
+
contact_id?: string | undefined;
|
|
16
|
+
remark?: string | undefined;
|
|
17
|
+
attachment_path?: string | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
voucherId: string;
|
|
20
|
+
url: string;
|
|
21
|
+
supplier: string;
|
|
22
|
+
contact: string;
|
|
23
|
+
voucherNumber: string;
|
|
24
|
+
voucherDate: string;
|
|
25
|
+
status: "unchecked";
|
|
26
|
+
gross: number;
|
|
27
|
+
tax: number;
|
|
28
|
+
net: number;
|
|
29
|
+
currency: string;
|
|
30
|
+
conversion: string | null;
|
|
31
|
+
declaredGross: number;
|
|
32
|
+
difference: number;
|
|
33
|
+
category: string;
|
|
34
|
+
attachment: string;
|
|
35
|
+
preflightNotes: string[];
|
|
36
|
+
note: string;
|
|
37
|
+
}> & {
|
|
38
|
+
execute(input: {
|
|
39
|
+
supplier_name: string;
|
|
40
|
+
voucher_number: string;
|
|
41
|
+
voucher_date: string;
|
|
42
|
+
due_date?: string | undefined;
|
|
43
|
+
currency: string;
|
|
44
|
+
bank_statement_eur_amount?: number | undefined;
|
|
45
|
+
total_gross_amount: number;
|
|
46
|
+
tax_groups: {
|
|
47
|
+
gross_amount: number;
|
|
48
|
+
tax_rate_percent: number;
|
|
49
|
+
category?: string | undefined;
|
|
50
|
+
}[];
|
|
51
|
+
category?: string | undefined;
|
|
52
|
+
contact_id?: string | undefined;
|
|
53
|
+
remark?: string | undefined;
|
|
54
|
+
attachment_path?: string | undefined;
|
|
55
|
+
}, ctx: import("eve/tools").ToolContext): Promise<{
|
|
56
|
+
voucherId: string;
|
|
57
|
+
url: string;
|
|
58
|
+
supplier: string;
|
|
59
|
+
contact: string;
|
|
60
|
+
voucherNumber: string;
|
|
61
|
+
voucherDate: string;
|
|
62
|
+
status: "unchecked";
|
|
63
|
+
gross: number;
|
|
64
|
+
tax: number;
|
|
65
|
+
net: number;
|
|
66
|
+
currency: string;
|
|
67
|
+
conversion: string | null;
|
|
68
|
+
declaredGross: number;
|
|
69
|
+
difference: number;
|
|
70
|
+
category: string;
|
|
71
|
+
attachment: string;
|
|
72
|
+
preflightNotes: string[];
|
|
73
|
+
note: string;
|
|
74
|
+
}>;
|
|
75
|
+
};
|
|
76
|
+
export default _default;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { eur } from "../lib/format.mjs";
|
|
8
|
+
import { approverPolicy, client, config } from "../lib/runtime.mjs";
|
|
9
|
+
import { MISSING_EUR_AMOUNT_INSTRUCTION, approvedPurchaseTarget, bookedGroupsOf, invoiceCurrencyOf, isAllowedAttachmentPath, isForeignCurrency, lacksEurAmount, purchaseSignatureOf, rememberPurchaseTarget, runPurchasePreflight, totalsOf } from "../lib/purchase.mjs";
|
|
10
|
+
import { describeWarning, isCorrectable, notesOf, rememberRejection, saveWarnings, wasRejected, wasShownOnCard } from "../lib/findings.mjs";
|
|
11
|
+
import { readJournal, writeJournal } from "../lib/journal.mjs";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { REVERSE_CHARGE_TAX_RATE, createVoucher, describeError, findPurchaseInvoiceByNumber, isReverseChargeCategory, round2, taxFromGross, uploadVoucherFile } from "@kevludwig/lexware-office";
|
|
14
|
+
import { defineTool } from "eve/tools";
|
|
15
|
+
const VOUCHER_STATUS = "unchecked";
|
|
16
|
+
const TAX_TYPE = "gross";
|
|
17
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
18
|
+
const TAX_RATES = [
|
|
19
|
+
0,
|
|
20
|
+
5,
|
|
21
|
+
7,
|
|
22
|
+
16,
|
|
23
|
+
19
|
|
24
|
+
];
|
|
25
|
+
const taxGroupSchema = z.object({
|
|
26
|
+
gross_amount: z.number().positive().describe("Bruttobetrag dieser Steuersatz-Gruppe in Rechnungswährung, wie auf der Rechnung — nicht umgerechnet"),
|
|
27
|
+
tax_rate_percent: z.number().refine((rate) => TAX_RATES.includes(rate), { message: `Steuersatz muss einer von ${TAX_RATES.join(", ")} sein` }).describe("Steuersatz dieser Gruppe in Prozent"),
|
|
28
|
+
category: z.string().trim().min(1).max(100).optional().describe("Buchungskategorie NUR dieser Gruppe (Name oder Id) — nur für einen auf mehrere Kategorien aufgeteilten Beleg; dann bei ALLEN Gruppen setzen und das Feld category weglassen. Bei gleichem Steuersatz mehrere Gruppen mit demselben Satz anlegen.")
|
|
29
|
+
}).strict();
|
|
30
|
+
const inputSchema = z.object({
|
|
31
|
+
supplier_name: z.string().trim().min(1).max(255).describe("Name des Lieferanten laut Rechnungskopf"),
|
|
32
|
+
voucher_number: z.string().trim().min(1).max(100).describe("Rechnungsnummer des Lieferanten. Pflicht — daran wird eine schon erfasste Rechnung erkannt."),
|
|
33
|
+
voucher_date: z.string().regex(ISO_DATE).describe("Rechnungsdatum, yyyy-MM-dd"),
|
|
34
|
+
due_date: z.string().regex(ISO_DATE).optional().describe("Fälligkeitsdatum, yyyy-MM-dd, wenn die Rechnung eines nennt"),
|
|
35
|
+
currency: z.string().trim().regex(/^[A-Za-z]{3}$/).describe("Währung der Rechnung als ISO-Code (EUR, USD, GBP, …). Alle Beträge sind in dieser Währung."),
|
|
36
|
+
bank_statement_eur_amount: z.number().positive().optional().describe("Nur bei currency ungleich EUR, dann Pflicht: der abgebuchte EUR-Betrag laut Kontoauszug, wie der Nutzer ihn nennt. Nie selbst umrechnen."),
|
|
37
|
+
total_gross_amount: z.number().positive().describe("Rechnungsendbetrag brutto in Rechnungswährung. Die Summe der tax_groups muss ihn ergeben."),
|
|
38
|
+
tax_groups: z.array(taxGroupSchema).min(1).max(5).describe("Die Beträge nach Steuersatz gruppiert, nicht die Einzelpositionen — 48 Artikel zu 19 % sind eine Gruppe. Mehrere Gruppen bei mehreren Steuersätzen oder für eine Aufteilung auf mehrere Kategorien (dann je Kategorie eine Gruppe mit category)."),
|
|
39
|
+
category: z.string().trim().min(1).max(100).optional().describe("Buchungskategorie (Name oder Id). Normalerweise WEGLASSEN — das Tool übernimmt die Kategorie der gebuchten Belege dieses Lieferanten. Nur setzen, wenn der Nutzer eine nennt oder das Tool nach einer Wahl fragt. Der Name muss wörtlich einer Kategorie des Kontos entsprechen."),
|
|
40
|
+
contact_id: z.string().uuid().optional().describe("Kontakt-Id des Lieferanten. Normalerweise weglassen — das Tool sucht selbst. Nur setzen, wenn die Suche mehrere findet und der Nutzer einen nennt."),
|
|
41
|
+
remark: z.string().trim().max(500).optional().describe("Bemerkung am Beleg. Ohne Angabe: Lieferant und Rechnungsnummer."),
|
|
42
|
+
attachment_path: z.string().trim().max(500).optional().describe("Sandbox-Pfad des Originals, wie beim hochgeladenen Dokument angegeben (/workspace/attachments/…). Wird als Anhang hochgeladen.")
|
|
43
|
+
}).strict().refine((input) => {
|
|
44
|
+
const withCategory = input.tax_groups.filter((group) => group.category).length;
|
|
45
|
+
return withCategory === 0 || withCategory === input.tax_groups.length;
|
|
46
|
+
}, { message: "category je Gruppe entweder bei allen tax_groups setzen oder bei keiner." }).refine((input) => !input.category || !input.tax_groups.some((group) => group.category), { message: "Entweder das Feld category ODER category je tax_group — nicht beides." }).refine((input) => !lacksEurAmount(input), { message: MISSING_EUR_AMOUNT_INSTRUCTION }).refine((input) => isForeignCurrency(input) || input.bank_statement_eur_amount === void 0, { message: "bank_statement_eur_amount nur bei einer Rechnung in Fremdwährung setzen." });
|
|
47
|
+
function mediaTypeOf(path) {
|
|
48
|
+
switch (path.slice(path.lastIndexOf(".") + 1).toLowerCase()) {
|
|
49
|
+
case "pdf": return "application/pdf";
|
|
50
|
+
case "png": return "image/png";
|
|
51
|
+
case "jpg":
|
|
52
|
+
case "jpeg": return "image/jpeg";
|
|
53
|
+
case "xml": return "application/xml";
|
|
54
|
+
default: return "application/octet-stream";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const basename = (path) => path.split("/").pop() || path;
|
|
58
|
+
var create_purchase_invoice_default = defineTool({
|
|
59
|
+
description: "Erfasst eine Eingangsrechnung (Lieferantenrechnung) als Beleg in Lexware Office und hängt das Original an. Beträge nach Steuersatz gruppiert, nicht als Einzelpositionen — die Artikel stehen im Anhang. Entsteht erst nach Freigabe, unter „Belege zur Prüfung“. Vorher wird geprüft, ob die Gruppen den Endbetrag ergeben und ob die Rechnungsnummer schon erfasst ist.",
|
|
60
|
+
inputSchema,
|
|
61
|
+
approval: {
|
|
62
|
+
request: async (ctx) => {
|
|
63
|
+
const input = ctx.toolInput;
|
|
64
|
+
if (!input?.voucher_number) return "user-approval";
|
|
65
|
+
if (lacksEurAmount(input)) return {
|
|
66
|
+
type: "denied",
|
|
67
|
+
reason: MISSING_EUR_AMOUNT_INSTRUCTION
|
|
68
|
+
};
|
|
69
|
+
const fallback = config().attachments;
|
|
70
|
+
const { warnings, supplier, category, groupCategories } = await runPurchasePreflight(input, {
|
|
71
|
+
signal: ctx.abortSignal,
|
|
72
|
+
readFileSize: async (path) => {
|
|
73
|
+
const bytes = await (await ctx.getSandbox()).readBinaryFile({ path });
|
|
74
|
+
if (bytes) return bytes.byteLength;
|
|
75
|
+
if (!fallback) return null;
|
|
76
|
+
if (fallback.size) return fallback.size(path);
|
|
77
|
+
return (await fallback.read(path))?.byteLength ?? null;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const signature = purchaseSignatureOf(input);
|
|
81
|
+
const correctable = warnings.filter(isCorrectable);
|
|
82
|
+
if (correctable.length > 0 && !wasRejected(signature)) {
|
|
83
|
+
rememberRejection(signature);
|
|
84
|
+
const totals = correctable.some((warning) => warning.kind === "voucher-total-mismatch");
|
|
85
|
+
const deviation = correctable.some((warning) => warning.kind === "category-deviation");
|
|
86
|
+
return {
|
|
87
|
+
type: "denied",
|
|
88
|
+
reason: correctable.map((warning) => describeWarning(warning).value).join(" — ") + (totals ? ". Prüfe die Beträge erneut: Der Rechnungsendbetrag ist brutto, die tax_groups müssen ihn in Summe ergeben. Korrigiere und rufe das Tool erneut auf. Bleibt die Abweichung, rufe es unverändert erneut auf — dann entscheidet der Nutzer." : deviation ? ". Setze category nur, wenn der Nutzer diese Kategorie genannt hat — sonst weglassen oder die übliche nehmen. Ist die Abweichung gewollt, rufe das Tool unverändert erneut auf — dann entscheidet der Nutzer an der Karte." : ". Rufe das Tool danach erneut auf; ohne Kategorie-Wahl entscheidet der Nutzer an der Karte.")
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
saveWarnings(ctx.callId, warnings);
|
|
92
|
+
rememberPurchaseTarget(ctx.callId, {
|
|
93
|
+
contactId: supplier.contactId,
|
|
94
|
+
contactName: supplier.contactName,
|
|
95
|
+
categoryId: category?.id,
|
|
96
|
+
categoryName: category?.name,
|
|
97
|
+
groupCategories: groupCategories?.map((match) => ({
|
|
98
|
+
id: match?.id,
|
|
99
|
+
name: match?.name
|
|
100
|
+
}))
|
|
101
|
+
});
|
|
102
|
+
const onCaptured = config().onPurchaseCaptured;
|
|
103
|
+
if (onCaptured && input.tax_groups?.length) {
|
|
104
|
+
const totals = totalsOf(bookedGroupsOf(input));
|
|
105
|
+
try {
|
|
106
|
+
await onCaptured({
|
|
107
|
+
callId: ctx.callId,
|
|
108
|
+
voucherNumber: input.voucher_number,
|
|
109
|
+
supplierName: supplier.contactName,
|
|
110
|
+
...totals
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.warn(`[lexware] onPurchaseCaptured failed: ${String(error)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return "user-approval";
|
|
117
|
+
},
|
|
118
|
+
response: approverPolicy
|
|
119
|
+
},
|
|
120
|
+
async execute(input, ctx) {
|
|
121
|
+
const bookedGroups = bookedGroupsOf(input);
|
|
122
|
+
const totals = totalsOf(bookedGroups);
|
|
123
|
+
const currency = invoiceCurrencyOf(input);
|
|
124
|
+
const invoiceGross = totalsOf(input.tax_groups).gross;
|
|
125
|
+
const conversion = isForeignCurrency(input) ? `${eur(invoiceGross, currency)} laut Rechnung, abgebucht ${eur(totals.gross)} laut Kontoauszug` : null;
|
|
126
|
+
const fallback = config().attachments;
|
|
127
|
+
let attachment = null;
|
|
128
|
+
let attachmentProblem = null;
|
|
129
|
+
if (input.attachment_path) {
|
|
130
|
+
if (!isAllowedAttachmentPath(input.attachment_path)) attachmentProblem = `„${input.attachment_path}" liegt nicht unter /workspace/attachments/`;
|
|
131
|
+
else {
|
|
132
|
+
let bytes = null;
|
|
133
|
+
try {
|
|
134
|
+
bytes = await (await ctx.getSandbox()).readBinaryFile({ path: input.attachment_path });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.warn(`[lexware] Sandbox read failed: ${describeError(error)}`);
|
|
137
|
+
}
|
|
138
|
+
if (!bytes && fallback) bytes = await fallback.read(input.attachment_path, ctx.abortSignal).catch(() => null);
|
|
139
|
+
if (bytes) attachment = {
|
|
140
|
+
bytes,
|
|
141
|
+
filename: basename(input.attachment_path),
|
|
142
|
+
mediaType: mediaTypeOf(input.attachment_path)
|
|
143
|
+
};
|
|
144
|
+
else attachmentProblem = `${basename(input.attachment_path)} liegt nicht mehr in der Sandbox${fallback ? " und nicht im Archiv" : ""}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const journal = await readJournal("purchase", ctx.callId);
|
|
148
|
+
let voucher;
|
|
149
|
+
let contactNote;
|
|
150
|
+
let categoryNote;
|
|
151
|
+
if (journal?.resourceId) {
|
|
152
|
+
voucher = {
|
|
153
|
+
id: journal.resourceId,
|
|
154
|
+
url: client().voucherUrl(journal.resourceId)
|
|
155
|
+
};
|
|
156
|
+
contactNote = `${input.supplier_name} (Wiederaufnahme — Beleg existierte bereits)`;
|
|
157
|
+
categoryNote = "wie beim ersten Lauf — in Lexware Office prüfen";
|
|
158
|
+
} else {
|
|
159
|
+
let existing = null;
|
|
160
|
+
try {
|
|
161
|
+
existing = await findPurchaseInvoiceByNumber(client(), input.voucher_number, ctx.abortSignal);
|
|
162
|
+
} catch {}
|
|
163
|
+
if (existing && !wasShownOnCard(ctx.callId, existing.url)) throw new Error(`Abgebrochen: Rechnung ${input.voucher_number} ist inzwischen bereits erfasst: ${existing.url}. Nichts angelegt. Schicke die Rechnung erneut, wenn trotzdem ein zweiter Beleg entstehen soll — dann steht der Fund auf der Karte.`);
|
|
164
|
+
const target = approvedPurchaseTarget(ctx.callId);
|
|
165
|
+
if (!target) throw new Error("Abgebrochen: Die an die Freigabe gebundene Lieferanten- und Kategorienwahl fehlt — nichts angelegt. Rufe das Tool erneut auf.");
|
|
166
|
+
const categoryOf = (index) => target.groupCategories?.[index]?.id ?? target.categoryId;
|
|
167
|
+
const reverseCharge = (index, rate) => rate === 0 && isReverseChargeCategory(target.groupCategories?.[index]?.name ?? target.categoryName);
|
|
168
|
+
categoryNote = target.groupCategories ? target.groupCategories.map((entry) => entry.name ?? "keine — in Lexware Office zuordnen").join(" / ") : target.categoryName ?? "keine — in Lexware Office zuordnen";
|
|
169
|
+
const common = {
|
|
170
|
+
type: "purchaseinvoice",
|
|
171
|
+
voucherStatus: VOUCHER_STATUS,
|
|
172
|
+
taxType: TAX_TYPE,
|
|
173
|
+
voucherNumber: input.voucher_number,
|
|
174
|
+
voucherDate: input.voucher_date,
|
|
175
|
+
dueDate: input.due_date,
|
|
176
|
+
totalGrossAmount: totals.gross,
|
|
177
|
+
totalTaxAmount: totals.tax,
|
|
178
|
+
remark: input.remark ?? `Lieferantenrechnung ${input.voucher_number} — ${target.contactName}${conversion ? ` · ${conversion}` : ""}`,
|
|
179
|
+
voucherItems: bookedGroups.map((group, index) => reverseCharge(index, group.tax_rate_percent) ? {
|
|
180
|
+
amount: group.gross_amount,
|
|
181
|
+
taxAmount: 0,
|
|
182
|
+
taxRatePercent: REVERSE_CHARGE_TAX_RATE,
|
|
183
|
+
categoryId: categoryOf(index)
|
|
184
|
+
} : {
|
|
185
|
+
amount: group.gross_amount,
|
|
186
|
+
taxAmount: taxFromGross(group.gross_amount, group.tax_rate_percent),
|
|
187
|
+
taxRatePercent: group.tax_rate_percent,
|
|
188
|
+
categoryId: categoryOf(index)
|
|
189
|
+
})
|
|
190
|
+
};
|
|
191
|
+
await writeJournal("purchase", ctx.callId, { status: "pending" });
|
|
192
|
+
voucher = target.contactId ? await createVoucher(client(), {
|
|
193
|
+
...common,
|
|
194
|
+
contactId: target.contactId
|
|
195
|
+
}, ctx.abortSignal) : await createVoucher(client(), {
|
|
196
|
+
...common,
|
|
197
|
+
useCollectiveContact: true,
|
|
198
|
+
contactName: target.contactName
|
|
199
|
+
}, ctx.abortSignal);
|
|
200
|
+
await writeJournal("purchase", ctx.callId, {
|
|
201
|
+
status: "created",
|
|
202
|
+
resourceId: voucher.id
|
|
203
|
+
});
|
|
204
|
+
contactNote = target.contactId ? target.contactName : `Sammellieferant (${target.contactName})`;
|
|
205
|
+
}
|
|
206
|
+
let attachmentResult = attachmentProblem ? `ohne Original — ${attachmentProblem}. In Lexware Office am Beleg nachreichen.` : "kein Anhang";
|
|
207
|
+
if (journal?.status === "attached") attachmentResult = "bereits angehängt (Wiederaufnahme)";
|
|
208
|
+
else if (attachment) try {
|
|
209
|
+
await uploadVoucherFile(client(), voucher.id, attachment, ctx.abortSignal);
|
|
210
|
+
await writeJournal("purchase", ctx.callId, {
|
|
211
|
+
status: "attached",
|
|
212
|
+
resourceId: voucher.id
|
|
213
|
+
});
|
|
214
|
+
attachmentResult = `${attachment.filename} angehängt`;
|
|
215
|
+
if (input.attachment_path) await fallback?.release?.(input.attachment_path).catch(() => {});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
attachmentResult = `${attachment.filename} konnte nicht angehängt werden: ${describeError(error)}`;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
voucherId: voucher.id,
|
|
221
|
+
url: voucher.url,
|
|
222
|
+
supplier: input.supplier_name,
|
|
223
|
+
contact: contactNote,
|
|
224
|
+
voucherNumber: input.voucher_number,
|
|
225
|
+
voucherDate: input.voucher_date,
|
|
226
|
+
status: VOUCHER_STATUS,
|
|
227
|
+
gross: totals.gross,
|
|
228
|
+
tax: totals.tax,
|
|
229
|
+
net: totals.net,
|
|
230
|
+
currency,
|
|
231
|
+
conversion,
|
|
232
|
+
declaredGross: input.total_gross_amount,
|
|
233
|
+
difference: round2(invoiceGross - input.total_gross_amount),
|
|
234
|
+
category: categoryNote,
|
|
235
|
+
attachment: attachmentResult,
|
|
236
|
+
preflightNotes: notesOf(ctx.callId),
|
|
237
|
+
note: `Der Beleg liegt unter „Belege zur Prüfung" und ist noch nicht gebucht. Netto ${eur(totals.net)}, USt. ${eur(totals.tax)}.`
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
export { create_purchase_invoice_default as default };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
declare const _default: import("eve/tools").ToolDefinition<{
|
|
2
|
+
customer_number?: number | undefined;
|
|
3
|
+
contact_id?: string | undefined;
|
|
4
|
+
items: {
|
|
5
|
+
name: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
unit: string;
|
|
8
|
+
net_price: number;
|
|
9
|
+
tax_rate: 0 | 7 | 19;
|
|
10
|
+
}[];
|
|
11
|
+
title?: string | undefined;
|
|
12
|
+
valid_until?: string | undefined;
|
|
13
|
+
}, Record<string, unknown>> & {
|
|
14
|
+
execute(input: {
|
|
15
|
+
customer_number?: number | undefined;
|
|
16
|
+
contact_id?: string | undefined;
|
|
17
|
+
items: {
|
|
18
|
+
name: string;
|
|
19
|
+
quantity: number;
|
|
20
|
+
unit: string;
|
|
21
|
+
net_price: number;
|
|
22
|
+
tax_rate: 0 | 7 | 19;
|
|
23
|
+
}[];
|
|
24
|
+
title?: string | undefined;
|
|
25
|
+
valid_until?: string | undefined;
|
|
26
|
+
}, ctx: import("eve/tools").ToolContext): Promise<Record<string, unknown>>;
|
|
27
|
+
};
|
|
28
|
+
export default _default;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { approverPolicy } from "../lib/runtime.mjs";
|
|
8
|
+
import { customerFields, executeSalesDocument, isCalendarDate, lineItemSchema, salesApproval, titleSchema } from "../lib/sales.mjs";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { defineTool } from "eve/tools";
|
|
11
|
+
const inputSchema = z.object({
|
|
12
|
+
...customerFields,
|
|
13
|
+
items: z.array(lineItemSchema).min(1).max(300).describe("Alle Positionen des Angebots"),
|
|
14
|
+
title: titleSchema("des Angebots"),
|
|
15
|
+
valid_until: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).refine(isCalendarDate, { message: "kein gültiges Kalenderdatum" }).optional().describe(`Gültig bis, yyyy-MM-dd. Ohne Angabe 30 Tage ab heute.`)
|
|
16
|
+
}).strict().refine((input) => input.customer_number === void 0 !== (input.contact_id === void 0), { message: "Genau eines von customer_number und contact_id angeben." });
|
|
17
|
+
var create_quotation_default = defineTool({
|
|
18
|
+
description: "Erstellt ein Angebot in Lexware Office und schreibt es direkt fest (es erhält eine Angebotsnummer; kein Entwurf). Kunde per Kundennummer oder contact_id — existiert er noch nicht, zuerst create_customer. Legt erst nach Freigabe an und prüft vorher, ob es für diesen Kunden schon ein Angebot mit gleicher Positionssumme gibt. Versendet wird aus Lexware Office.",
|
|
19
|
+
inputSchema,
|
|
20
|
+
approval: {
|
|
21
|
+
request: salesApproval("quotation"),
|
|
22
|
+
response: approverPolicy
|
|
23
|
+
},
|
|
24
|
+
async execute(input, ctx) {
|
|
25
|
+
return executeSalesDocument("quotation", input, ctx);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
export { create_quotation_default as default };
|