Files
FEDEO/frontend/pages/createDocument/edit/[[id]].vue
florianfederspiel 76e82eae4f feat(accounting): fehlende PDF von Ausgangsbelegen nacherzeugen
Auf der Belegansicht erscheint "PDF nacherzeugen", wenn ein
fertiggestellter Beleg kein angehängtes Dokument hat. Die PDF wird mit
derselben Logik wie beim Fertigstellen erzeugt und angehängt, ohne den
Datensatz zu speichern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:40:25 +01:00

4049 lines
160 KiB
Vue

<script setup>
import dayjs from "dayjs"
import { parseDate } from "@internationalized/date"
import { v4 as uuidv4 } from 'uuid';
import {useFunctions} from "~/composables/useFunctions.js";
import EntityModalButtons from "~/components/EntityModalButtons.vue";
import { documentTemplateHandlebars } from "~/utils/handlebars";
const dataStore = useDataStore()
const profileStore = useProfileStore()
const route = useRoute()
const router = useRouter()
const modal = useModal()
const auth = useAuthStore()
const toast = useToast()
const isTemplateMode = computed(() => Boolean(route.query.templateFromDocument || route.query.mode === "template"))
const templateName = ref("")
const quoteLikeDocumentTypes = ["quotes", "costEstimates"]
const deliveryNoteLikeDocumentTypes = ["deliveryNotes", "packingSlips"]
const documentStorageFallbackTypes = {
costEstimates: "quotes",
packingSlips: "deliveryNotes",
advanceInvoices: "invoices",
cancellationInvoices: "invoices"
}
const textTemplateFallbackTypes = {
serialInvoices: "invoices",
costEstimates: "quotes",
packingSlips: "deliveryNotes"
}
const guard = ref(true)
const itemInfo = ref({
type: "invoices",
state: "Entwurf",
taxType: "Standard",
customer: null,
contact: null,
address: {
street: null,
special: null,
zip: null,
city: null,
country: null,
},
project: null,
costcentre: null,
documentNumber: null,
documentNumberTitle: "Rechnungsnummer",
documentDate: dayjs(),
deliveryDate: dayjs(),
deliveryDateEnd: null,
deliveryDateType: "Lieferdatum",
dateOfPerformance: null,
paymentDays: auth.activeTenantData.standardPaymentDays,
payment_type: "transfer",
outgoingsepamandate: null,
availableInPortal: false,
customSurchargePercentage: 0,
created_by: auth.user.id,
title: null,
description: null,
startText: null,
endText: null,
rows: [],
contactPerson: auth.user.id,
contactPersonName: null,
contactTel: null,
contactEMail: null,
serialConfig: {
firstExecution: "",
intervall: "monatlich",
executionUntil: "",
active: true,
dateDirection: "Rückwirkend",
},
letterhead: null,
agriculture: {},
usedAdvanceInvoices: []
})
console.log(auth.activeTenantData)
const letterheads = ref([])
const createddocuments = ref([])
const projects = ref([])
const plants = ref([])
const costcentres = ref([])
const products = ref([])
const productcategories = ref([])
const selectedProductcategorie = ref(null)
const services = ref([])
const servicecategories = ref([])
const selectedServicecategorie = ref(null)
const collapsedTitleRowIds = ref(new Set())
const customers = ref([])
const contacts = ref([])
const contracts = ref([])
const outgoingsepamandates = ref([])
const bankaccounts = ref([])
const texttemplates = ref([])
const units = ref([])
const tenantUsers = ref([])
const originalDocumentType = ref(null)
const canUseAdvanceInvoice = computed(() => originalDocumentType.value === 'advanceInvoices' || (
route.query.type === 'advanceInvoices'
&& route.query.loadMode === 'advanceInvoice'
&& Number.isSafeInteger(Number(route.query.project))
&& Number(route.query.project) > 0
&& !route.params.id
&& !route.query.createddocument
&& !route.query.optionsToImport
&& !route.query.linkedDocuments
&& !route.query.templateId
&& !isTemplateMode.value
))
const documentTypeItems = computed(() => Object.keys(dataStore.documentTypesForCreation)
.filter(key => key !== 'advanceInvoices' || canUseAdvanceInvoice.value)
.map((key) => ({
label: dataStore.documentTypesForCreation[key].labelSingle,
type: key
})))
const validateAdvanceInvoiceCreation = () => {
if (itemInfo.value.type !== 'advanceInvoices' || isTemplateMode.value) return true
if (canUseAdvanceInvoice.value && (route.params.id || normalizeEntityId(itemInfo.value.project))) return true
toast.add({
title: 'Abschlagsrechnung nur im Projekt erstellen',
description: 'Bitte den Button „Abschlagsrechnung“ innerhalb eines Projekts verwenden.',
color: 'error'
})
return false
}
const taxTypeItems = [
{ key: 'Standard', label: 'Standard' },
{ key: '13b UStG', label: '13b UStG' },
{ key: '19 UStG', label: '19 UStG Kleinunternehmer' },
{ key: '12.3 UStG', label: '12.3 UStG' }
]
const titleLevelItems = [
{ label: 'Titel Ebene 1', value: 1 },
{ label: 'Untertitel Ebene 2', value: 2 },
{ label: 'Untertitel Ebene 3', value: 3 }
]
const deliveryDateTypeItems = ['Lieferdatum', 'Lieferzeitraum', 'Leistungsdatum', 'Leistungszeitraum', 'Kein Lieferdatum anzeigen']
const paymentTypeItems = [
{ key: 'transfer', label: 'Überweisung' },
{ key: 'direct-debit', label: 'SEPA Lastschrift' }
]
const serialIntervalItems = ['wöchentlich', '2 - wöchentlich', 'monatlich', 'vierteljährlich', 'halbjährlich', 'jährlich']
const serialDateDirectionItems = ['Rückwirkend', 'Im Voraus']
const taxPercentItems = [19, 7, 0]
const selectedCustomer = computed(() => customers.value.find(i => i.id === itemInfo.value.customer) || null)
const findById = (items, id) => {
if (id === null || typeof id === "undefined") return null
return items.find((item) => item.id === id) || null
}
const getContactName = (id) => findById(contacts.value, id)?.fullName || "Kontakt nicht gefunden"
const getContractName = (id) => findById(contracts.value, id)?.name || "Vertrag nicht gefunden"
const getContractNumber = (id) => findById(contracts.value, id)?.contractNumber || "Vertrag nicht gefunden"
const getLetterheadName = (id) => findById(letterheads.value, id)?.name || "Briefpapier nicht gefunden"
const getPlantName = (id) => findById(plants.value, id)?.name || "Objekt nicht gefunden"
const getProjectName = (id) => findById(projects.value, id)?.name || "Projekt nicht gefunden"
const getCostCentreName = (id) => {
const costcentre = findById(costcentres.value, id)
return costcentre ? [costcentre.number, costcentre.name].filter(Boolean).join(" - ") : "Keine Kostenstelle ausgewählt"
}
const getSelectedLetterhead = () => findById(letterheads.value, itemInfo.value.letterhead)
const normalizeExternalUrl = (value) => {
if (!value || typeof value !== "string") return null
const trimmedValue = value.trim()
if (!trimmedValue) return null
if (/^https?:\/\//i.test(trimmedValue)) return trimmedValue
return `https://${trimmedValue}`
}
const getSelectedProduct = (row) => products.value.find((item) => item.id === row.product) || null
const formatNumberLikeValue = (value) => {
if (value === null || typeof value === 'undefined' || value === '') return '-'
if (typeof value === 'string' || typeof value === 'number') return String(value)
if (typeof value === 'object') {
const directValue = value.value ?? value.number ?? value.currentNumber ?? value.customerNumber ?? value.id
if (directValue !== null && typeof directValue !== 'undefined' && directValue !== '') {
return String(directValue)
}
if (typeof value.prefix !== 'undefined' || typeof value.nextNumber !== 'undefined' || typeof value.suffix !== 'undefined') {
return `${value.prefix || ''}${value.nextNumber || ''}${value.suffix || ''}` || '-'
}
}
return '-'
}
const normalizeTaxTypeValue = (value) => {
const normalized = typeof value === "string" ? value.trim() : value
return taxTypeItems.find((item) => item.key === normalized)?.key || "Standard"
}
const getCalendarValue = (value) => {
if (!value) return undefined
const formatted = dayjs(value).format('YYYY-MM-DD')
return formatted ? parseDate(formatted) : undefined
}
const syncDeliveryDateEndWithStart = () => {
if (!['Lieferzeitraum', 'Leistungszeitraum'].includes(itemInfo.value.deliveryDateType)) return
if (!itemInfo.value.deliveryDate) return
if (!itemInfo.value.deliveryDateEnd || dayjs(itemInfo.value.deliveryDateEnd).isBefore(dayjs(itemInfo.value.deliveryDate), 'day')) {
itemInfo.value.deliveryDateEnd = dayjs(itemInfo.value.deliveryDate).toDate()
}
}
const setCalendarField = (field, value) => {
itemInfo.value[field] = value ? dayjs(value.toString()).toDate() : null
if (field === 'deliveryDate') {
syncDeliveryDateEndWithStart()
}
}
const setSerialCalendarField = (field, value) => {
itemInfo.value.serialConfig[field] = value ? dayjs(value.toString()).toDate() : null
}
const getDateButtonLabel = (value) => value ? dayjs(value).format('DD.MM.YYYY') : 'Datum auswählen'
const setCalendarFieldToToday = (field) => {
itemInfo.value[field] = dayjs().toDate()
if (field === 'deliveryDate') {
syncDeliveryDateEndWithStart()
}
}
const setSerialCalendarFieldToToday = (field) => {
itemInfo.value.serialConfig[field] = dayjs().toDate()
}
watch(() => itemInfo.value.deliveryDateType, () => {
syncDeliveryDateEndWithStart()
})
const setupData = async () => {
letterheads.value = (await useEntities("letterheads").select("*")).filter(i => {
const documentTypes = Array.isArray(i.documentTypes) ? i.documentTypes : []
return documentTypes.length === 0 || documentTypes.includes(itemInfo.value.type)
})
createddocuments.value = await useEntities("createddocuments").select("*")
projects.value = await useEntities("projects").select("*")
plants.value = await useEntities("plants").select("*")
costcentres.value = await useEntities("costcentres").select("*")
services.value = await useEntities("services").select("*")
servicecategories.value = await useEntities("servicecategories").select("*")
products.value = await useEntities("products").select("*")
productcategories.value = await useEntities("productcategories").select("*")
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("*")
try {
bankaccounts.value = await useEntities("bankaccounts").select("*")
} catch (error) {
console.warn("Bankkonten konnten nicht für den Zahlungs-QR-Code geladen werden", error)
bankaccounts.value = []
}
texttemplates.value = await useEntities("texttemplates").select("*")
units.value = await useEntities("units").selectSpecial("*")
tenantUsers.value = (await useNuxtApp().$api(`/api/tenant/users`, {
method: "GET"
})).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 getSelectedSepaMandate = () => {
return availableSepaMandates.value.find((mandate) => mandate.id === itemInfo.value.outgoingsepamandate) || null
}
const mapContractPaymentType = (paymentType) => {
if (paymentType === "Einzug") return "direct-debit"
if (paymentType === "Überweisung") return "transfer"
return null
}
const applyContractPaymentData = () => {
const selectedContract = contracts.value.find((contract) => contract.id === itemInfo.value.contract)
if (!selectedContract) return
const mappedPaymentType = mapContractPaymentType(selectedContract.paymentType)
if (mappedPaymentType) {
itemInfo.value.payment_type = mappedPaymentType
}
const contractMandateId = selectedContract.outgoingsepamandate?.id || selectedContract.outgoingsepamandate
if (mappedPaymentType === "direct-debit" && contractMandateId) {
itemInfo.value.outgoingsepamandate = contractMandateId
} else if (mappedPaymentType === "transfer") {
itemInfo.value.outgoingsepamandate = null
}
}
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.contract,
() => applyContractPaymentData()
)
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
if (value instanceof Date) return null
if (typeof value === "string") {
const normalized = value.trim()
if (!/^\d+$/.test(normalized) && dayjs(normalized).isValid()) return null
}
return typeof value === "object" ? (value.id ?? null) : value
}
const normalizeUnitId = (unit) => {
const value = unit && typeof unit === "object" ? unit.id : unit
if (typeof value !== "number" && typeof value !== "string") return null
const id = Number(value)
return Number.isSafeInteger(id) && id > 0 ? id : null
}
const normalizeCreatedDocumentRow = (row) => {
let normalizedRow = row
if (typeof normalizedRow === "string") {
try {
normalizedRow = JSON.parse(normalizedRow)
} catch {
normalizedRow = {
id: uuidv4(),
mode: "text",
text: normalizedRow,
}
}
}
if (!normalizedRow || typeof normalizedRow !== "object" || Array.isArray(normalizedRow)) {
normalizedRow = {
id: uuidv4(),
mode: "text",
text: String(row ?? ""),
}
}
return {
...normalizedRow,
id: normalizedRow.id || uuidv4(),
unit: normalizeUnitId(normalizedRow.unit),
costCentre: normalizedRow.costCentre || normalizedRow.costcentre || null,
purchasePrice: Number(normalizedRow.purchasePrice || 0),
linkedEntitys: Array.isArray(normalizedRow.linkedEntitys) ? normalizedRow.linkedEntitys : [],
}
}
const normalizeCreatedDocumentRows = (rows) => Array.isArray(rows)
? rows.map((row) => normalizeCreatedDocumentRow(row))
: []
const isImportedAdvanceInvoiceRow = (row) => {
return Array.isArray(row.linkedEntitys)
&& row.linkedEntitys.some(i => i.type === "createddocuments" && i.subtype === "advanceInvoices")
}
const setupPage = async () => {
await setupData()
if (productcategories.value.length > 0) selectedProductcategorie.value = productcategories.value[0].id
if (servicecategories.value.length > 0) selectedServicecategorie.value = servicecategories.value[0].id
if (route.params) {
if (route.params.id) {
console.log(route.params)
itemInfo.value = await useEntities("createddocuments").selectSingle(route.params.id,'',false)
originalDocumentType.value = itemInfo.value.type
itemInfo.value.rows = normalizeCreatedDocumentRows(itemInfo.value.rows)
itemInfo.value.taxType = normalizeTaxTypeValue(itemInfo.value.taxType)
await setContactPersonData()
checkCompatibilityWithInputPrice()
}
if (!itemInfo.value.deliveryDateType) itemInfo.value.deliveryDateType = "Lieferdatum"
itemInfo.value.rows = normalizeCreatedDocumentRows(itemInfo.value.rows)
if (itemInfo.value.rows.find(i => i.agriculture)) {
processDieselPosition()
}
}
if (route.query) {
if (route.query.type) itemInfo.value.type = route.query.type
if (route.query.templateFromDocument) {
const sourceDocument = await useEntities("createddocuments").selectSingle(route.query.templateFromDocument, '', false)
const sourceData = JSON.parse(JSON.stringify(sourceDocument || {}))
;["id", "createdAt", "tenant", "documentNumber", "documentDate", "state", "customer", "contact", "address", "project", "costcentre", "createddocument", "availableInPortal", "archived", "statementallocations", "files", "linkedDocument", "createddocuments", "serialexecution"].forEach((key) => delete sourceData[key])
Object.assign(itemInfo.value, sourceData)
itemInfo.value.type = route.query.type || sourceDocument?.type || itemInfo.value.type
templateName.value = `${dataStore.documentTypesForCreation[itemInfo.value.type]?.labelSingle || "Dokument"} Vorlage`
}
if (route.query.templateId) {
const template = await useEntities("documenttemplates").selectSingle(route.query.templateId, '', false)
if (template?.templateData) {
Object.assign(itemInfo.value, JSON.parse(JSON.stringify(template.templateData)))
itemInfo.value.type = template.documentType
templateName.value = template.name
}
}
if (!itemInfo.value.startText && !itemInfo.value.endText) {
setDocumentTypeConfig(true)
} else {
setDocumentTypeConfig(false)
}
if (route.query.linkedDocuments) {
console.log(route.query.loadMode)
if (route.query.loadMode === "deliveryNotes") {
let linkedDocuments = (await useEntities("createddocuments").select()).filter(i => JSON.parse(route.query.linkedDocuments).includes(i.id))
if (linkedDocuments.length === 0) return
//TODO: Implement Checking for Same Customer, Contact and Project
itemInfo.value.customer = normalizeEntityId(linkedDocuments[0].customer)
itemInfo.value.project = normalizeEntityId(linkedDocuments[0].project)
itemInfo.value.contact = normalizeEntityId(linkedDocuments[0].contact)
await setCustomerData(null, true)
let firstDate = null
let lastDate = null
linkedDocuments.forEach(doc => {
let lastId = 0
itemInfo.value.rows.forEach(row => {
if (row.id > lastId) lastId = row.id
})
if (dayjs(doc.documentDate).isBefore(firstDate) || !firstDate) firstDate = doc.documentDate
if (dayjs(doc.documentDate).isAfter(lastDate) || !lastDate) lastDate = doc.documentDate
itemInfo.value.rows.push(...[
{
id: uuidv4(),
mode: "title",
text: `${doc.title} vom ${dayjs(doc.documentDate).format("DD.MM.YYYY")}`
},
...normalizeCreatedDocumentRows(doc.rows)
])
})
itemInfo.value.deliveryDateType = "Leistungszeitraum"
itemInfo.value.deliveryDate = firstDate
itemInfo.value.deliveryDateEnd = lastDate
itemInfo.value.rows.forEach(row => {
row.discountPercent = 0
setRowData(row)
})
setPosNumbers()
console.log(linkedDocuments)
if (linkedDocuments.find(i => normalizeCreatedDocumentRows(i.rows).find(x => x.agriculture?.dieselUsage))) {
console.log("has diesel")
//Remove Existing Total Diesel Pos
itemInfo.value.rows = itemInfo.value.rows.filter(i => i.key !== "dieselPos")
//Remove Existing Total Ad Blue Pos
itemInfo.value.rows = itemInfo.value.rows.filter(i => i.key !== "adbluePos")
//Add Total Title
itemInfo.value.rows.push({
id: uuidv4(),
mode: "title",
text: "Allgemein"
})
processDieselPosition()
}
}
else if (route.query.loadMode === "finalInvoice") {
let linkedDocuments = (await useEntities("createddocuments").select()).filter(i => JSON.parse(route.query.linkedDocuments).includes(i.id))
if (linkedDocuments.length === 0) return
//TODO: Implement Checking for Same Customer, Contact and Project
console.log(linkedDocuments)
itemInfo.value.customer = normalizeEntityId(linkedDocuments[0].customer)
itemInfo.value.project = normalizeEntityId(linkedDocuments[0].project)
itemInfo.value.contact = normalizeEntityId(linkedDocuments[0].contact)
await setCustomerData(null, true)
for await (const doc of linkedDocuments.filter(i => i.type === "confirmationOrders")) {
let linkedDocument = await useEntities("createddocuments").selectSingle(doc.id)
itemInfo.value.rows.push({
id: uuidv4(),
mode: "title",
text: linkedDocument.title,
})
itemInfo.value.rows.push(...normalizeCreatedDocumentRows(linkedDocument.rows))
}
for await (const doc of linkedDocuments.filter(i => quoteLikeDocumentTypes.includes(i.type))) {
let linkedDocument = await useEntities("createddocuments").selectSingle(doc.id)
itemInfo.value.rows.push({
id: uuidv4(),
mode: "title",
text: linkedDocument.title,
})
itemInfo.value.rows.push(...normalizeCreatedDocumentRows(linkedDocument.rows))
}
itemInfo.value.rows.push({
id: uuidv4(),
mode: "title",
text: "Abschlagsrechnungen",
})
for await (const doc of linkedDocuments.filter(i => i.type === "advanceInvoices")) {
const advanceInvoiceNet = useSum().getCreatedDocumentSumDetailed(doc).totalNet * -1
itemInfo.value.rows.push({
mode: "free",
text: `Abschlagsrechnung ${doc.documentNumber}`,
quantity: 1,
taxPercent: 19, // TODO TAX PERCENTAGE
discountPercent: 0,
unit: 10,
inputPrice: advanceInvoiceNet,
price: advanceInvoiceNet,
linkedEntitys: [
{
type: "createddocuments",
subtype: "advanceInvoices",
id: doc.id
}
],
editDisabled: true
})
updateCustomSurcharge()
}
setPosNumbers()
}
}
if (route.query.createddocument) {
itemInfo.value.createddocument = route.query.createddocument
let linkedDocument = await useEntities("createddocuments").selectSingle(itemInfo.value.createddocument,'',false)
if (route.query.optionsToImport) {
//Import only true
let optionsToImport = JSON.parse(route.query.optionsToImport)
console.log(optionsToImport)
console.log(linkedDocument)
if (optionsToImport.taxType) itemInfo.value.taxType = normalizeTaxTypeValue(linkedDocument.taxType)
if (optionsToImport.customer) itemInfo.value.customer = linkedDocument.customer
if (optionsToImport.letterhead) itemInfo.value.letterhead = linkedDocument.letterhead
if (optionsToImport.contact) itemInfo.value.contact = linkedDocument.contact
if (optionsToImport.deliveryDateType) itemInfo.value.deliveryDateType = linkedDocument.deliveryDateType
if (optionsToImport.deliveryDate) itemInfo.value.deliveryDate = linkedDocument.deliveryDate
if (optionsToImport.deliveryDateEnd) itemInfo.value.deliveryDateEnd = linkedDocument.deliveryDateEnd
if (optionsToImport.documentDate) itemInfo.value.documentDate = linkedDocument.documentDate
if (optionsToImport.paymentDays) itemInfo.value.paymentDays = linkedDocument.paymentDays
if (optionsToImport.payment_type) itemInfo.value.payment_type = linkedDocument.payment_type
if (optionsToImport.customSurchargePercentage) itemInfo.value.customSurchargePercentage = linkedDocument.customSurchargePercentage
if (optionsToImport.contactPerson) itemInfo.value.contactPerson = linkedDocument.contactPerson
if (optionsToImport.plant) itemInfo.value.plant = linkedDocument.plant
if (optionsToImport.project) itemInfo.value.project = linkedDocument.project
if (optionsToImport.costcentre) itemInfo.value.costcentre = linkedDocument.costcentre
if (optionsToImport.title) itemInfo.value.title = linkedDocument.title
if (optionsToImport.description) itemInfo.value.description = linkedDocument.description
if (optionsToImport.startText) itemInfo.value.startText = linkedDocument.startText
if (optionsToImport.rows) itemInfo.value.rows = normalizeCreatedDocumentRows(linkedDocument.rows)
if (optionsToImport.endText) itemInfo.value.endText = linkedDocument.endText
} else {
// Import all
if (process.dev) console.log(linkedDocument)
itemInfo.value.taxType = normalizeTaxTypeValue(linkedDocument.taxType)
itemInfo.value.customer = linkedDocument.customer
await setCustomerData(null, true)
itemInfo.value.letterhead = linkedDocument.letterhead
itemInfo.value.contact = linkedDocument.contact
itemInfo.value.deliveryDateType = linkedDocument.deliveryDateType
itemInfo.value.deliveryDate = linkedDocument.deliveryDate
itemInfo.value.deliveryDateEnd = linkedDocument.deliveryDateEnd
itemInfo.value.documentDate = linkedDocument.documentDate
itemInfo.value.paymentDays = linkedDocument.paymentDays
itemInfo.value.payment_type = linkedDocument.payment_type
itemInfo.value.customSurchargePercentage = linkedDocument.customSurchargePercentage
itemInfo.value.contactPerson = linkedDocument.contactPerson
itemInfo.value.plant = linkedDocument.plant
itemInfo.value.project = linkedDocument.project
itemInfo.value.costcentre = linkedDocument.costcentre
itemInfo.value.title = linkedDocument.title
itemInfo.value.description = linkedDocument.description
itemInfo.value.startText = linkedDocument.startText
itemInfo.value.rows = normalizeCreatedDocumentRows(linkedDocument.rows)
itemInfo.value.endText = linkedDocument.endText
}
checkCompatibilityWithInputPrice()
if (route.query.loadMode === "storno") {
itemInfo.value.rows.forEach(row => {
row.price = row.price * -1
})
itemInfo.value.documentDate = dayjs()
itemInfo.value.usedAdvanceInvoices = linkedDocument.usedAdvanceInvoices
itemInfo.value.description = `Stornorechnung zu Rechnung ${linkedDocument.documentNumber} vom ${dayjs(linkedDocument.documentDate).format('DD.MM.YYYY')}`
itemInfo.value.type = "cancellationInvoices"
setDocumentTypeConfig(true)
}
await setCustomerData(null, true)
}
if (route.query.project) {
itemInfo.value.project = Number(route.query.project)
let project = await useEntities("projects").selectSingle(itemInfo.value.project)
if (!itemInfo.value.description) {
itemInfo.value.description = project.customerRef
}
}
if (route.query.plant) {
itemInfo.value.plant = Number(route.query.plant)
}
if (route.query.contact) itemInfo.value.contact = Number(route.query.contact)
if (route.query.customer) {
itemInfo.value.customer = Number(route.query.customer)
setCustomerData()
}
}
itemInfo.value.rows = normalizeCreatedDocumentRows(itemInfo.value.rows)
if (!validateAdvanceInvoiceCreation()) {
await router.replace('/createDocument')
return
}
if (route.query.regeneratePdf && route.params.id) {
await regenerateMissingPdf()
return
}
loaded.value = true
}
setupPage()
const setDocumentTypeConfig = (withTexts = false) => {
if (itemInfo.value.type === "invoices" || itemInfo.value.type === "advanceInvoices" || itemInfo.value.type === "serialInvoices" || itemInfo.value.type === "cancellationInvoices") {
itemInfo.value.documentNumberTitle = "Rechnungsnummer"
itemInfo.value.title = `Rechnung-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}`
} else if (quoteLikeDocumentTypes.includes(itemInfo.value.type)) {
const titlePrefix = itemInfo.value.type === "costEstimates" ? "Kostenschätzung" : "Angebot"
itemInfo.value.documentNumberTitle = itemInfo.value.type === "costEstimates" ? "Kostenschätzungsnummer" : "Angebotsnummer"
itemInfo.value.title = `${titlePrefix}-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}`
} else if (deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type)) {
const titlePrefix = itemInfo.value.type === "packingSlips" ? "Packschein" : "Lieferschein"
itemInfo.value.documentNumberTitle = itemInfo.value.type === "packingSlips" ? "Packscheinnummer" : "Lieferscheinnummer"
itemInfo.value.title = `${titlePrefix}-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}`
} else if (itemInfo.value.type === "confirmationOrders") {
itemInfo.value.documentNumberTitle = "Auftragsbestätigungsnr."
itemInfo.value.title = `Auftragsbestätigung-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}`
}
if (withTexts) {
const startTemplate = getTextTemplateByType(itemInfo.value.type).find(i => i.default && i.pos === "startText")
const endTemplate = getTextTemplateByType(itemInfo.value.type).find(i => i.default && i.pos === "endText")
itemInfo.value.startText = startTemplate?.text || null
itemInfo.value.endText = endTemplate?.text || null
//itemInfo.value.startText = texttemplates.value.find(i => i.documentType === itemInfo.value.type && i.default && i.pos === "startText").text
//itemInfo.value.endText = texttemplates.value.find(i => i.documentType === itemInfo.value.type && i.default && i.pos === "endText").text
}
if (!getSelectedLetterhead()) {
itemInfo.value.letterhead = letterheads.value[0]?.id || null
}
if (itemInfo.value.type === "advanceInvoices" && !itemInfo.value.rows.find(i => i.text === "Abschlagszahlung")) {
itemInfo.value.rows.push({
mode: "free",
text: "Abschlagszahlung",
quantity: 1,
unit: 10,
price: 0,
taxPercent: 19,
discountPercent: 0,
advanceInvoiceData: advanceInvoiceData.value
})
setPosNumbers()
}
}
const setTaxType = () => {
if (itemInfo.value.taxType === "19 UStG") {
itemInfo.value.rows.forEach(row => {
row.taxPercent = 0
})
} else if (itemInfo.value.taxType === "13b UStG") {
itemInfo.value.rows.forEach(row => {
row.taxPercent = 0
})
} else if (itemInfo.value.taxType === "12.3 UStG") {
itemInfo.value.rows.forEach(row => {
row.taxPercent = 0
})
}
}
const setCustomerData = async (customerId, loadOnlyAdress = false) => {
if (customerId) {
itemInfo.value.customer = customerId
}
customers.value = await useEntities("customers").select()
let customer = customers.value.find(i => i.id === itemInfo.value.customer)
console.log(customer)
if(!loadOnlyAdress) {
itemInfo.value.contact = null
}
if (customer) {
itemInfo.value.address.street = customer.infoData.street
itemInfo.value.address.zip = customer.infoData.zip
itemInfo.value.address.city = customer.infoData.city
itemInfo.value.address.special = customer.infoData.special
itemInfo.value.address.country = customer.infoData.country
if (!loadOnlyAdress && customer.customPaymentDays) itemInfo.value.paymentDays = customer.customPaymentDays
if (!loadOnlyAdress && customer.custom_payment_type) itemInfo.value.payment_type = customer.custom_payment_type
if (!loadOnlyAdress) {
itemInfo.value.taxType = normalizeTaxTypeValue(customer.customTaxType)
setTaxType()
}
if (!loadOnlyAdress && customer.customSurchargePercentage) {
itemInfo.value.customSurchargePercentage = customer.customSurchargePercentage
updateCustomSurcharge()
}
if (!loadOnlyAdress && contacts.value.filter(i => i.customer === itemInfo.value.customer).length === 1) {
itemInfo.value.contact = contacts.value.filter(i => i.customer === itemInfo.value.customer)[0].id
}
}
}
const setContactPersonData = async () => {
//console.log(itemInfo.value.contactPerson) //TODO: BACKEND CHANGE Set Profile
if(itemInfo.value.created_by) {
let profile = (await useNuxtApp().$api(`/api/user/${itemInfo.value.created_by}`, {
method: "GET"
})).profile
itemInfo.value.contactPersonName = profile.full_name
itemInfo.value.contactTel = profile.mobile_tel || profile.fixed_tel || ""
itemInfo.value.contactEMail = profile.email
}
}
const showAdvanceInvoiceCalcModal = ref(false)
const advanceInvoiceData = ref({
totalSumNet: 0,
partPerPecentage: 0,
part: 0
})
const importPositions = () => {
if (itemInfo.value.type === 'advanceInvoices') {
if (advanceInvoiceData.value.totalSumNet !== 0 && advanceInvoiceData.value.partPerPecentage !== 0 && advanceInvoiceData.value.part !== 0) {
showAdvanceInvoiceCalcModal.value = false
let lastId = 0
itemInfo.value.rows.forEach(row => {
if (row.id > lastId) lastId = row.id
})
itemInfo.value.rows.push({
id: lastId + 1,
mode: "free",
text: "Abschlagszahlung",
quantity: 1,
unit: 10,
price: advanceInvoiceData.value.part,
taxPercent: 19,
discountPercent: 0,
costCentre: itemInfo.value.costcentre,
advanceInvoiceData: advanceInvoiceData.value
})
setPosNumbers()
}
}
}
const getRowAmount = (row) => {
return String(Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100)).toFixed(2)).replace('.', ',')
}
const getRowAmountUndiscounted = (row) => {
return String(Number(Number(row.quantity) * Number(row.price)).toFixed(2)).replace('.', ',')
}
const positionAddOptions = [
{ mode: 'service', label: 'Leistung', icon: 'i-heroicons-wrench-screwdriver' },
{ mode: 'normal', label: 'Artikel', icon: 'i-heroicons-cube' },
{ mode: 'free', label: 'Freie Position', icon: 'i-heroicons-pencil-square' },
{ mode: 'pagebreak', label: 'Seitenumbruch', icon: 'i-heroicons-document-minus' },
{ mode: 'title', label: 'Titel', icon: 'i-heroicons-bars-3-bottom-left' },
{ mode: 'subtitle', label: 'Untertitel', icon: 'i-heroicons-list-bullet' },
{ mode: 'text', label: 'Text', icon: 'i-heroicons-document-text' }
]
const createPositionRow = (mode, taxPercentage) => {
if (mode === 'free') {
const rowData = {
id: uuidv4(),
mode: "free",
text: "",
quantity: 1,
unit: 1,
inputPrice: 0,
purchasePrice: 0,
price: 0,
taxPercent: taxPercentage,
discountPercent: 0,
costCentre: itemInfo.value.costcentre,
linkedEntitys: []
}
return {...rowData, ...auth.activeTenantData.extraModules.includes("agriculture") ? {agriculture: {}} : {}}
}
if (mode === 'normal') {
return {
id: uuidv4(),
mode: "normal",
quantity: 1,
inputPrice: 0,
price: 0,
taxPercent: taxPercentage,
discountPercent: 0,
unit: 1,
costCentre: itemInfo.value.costcentre,
linkedEntitys: []
}
}
if (mode === 'service') {
const rowData = {
id: uuidv4(),
mode: "service",
quantity: 1,
inputPrice: 0,
price: 0,
taxPercent: taxPercentage,
discountPercent: 0,
unit: 1,
costCentre: itemInfo.value.costcentre,
linkedEntitys: []
}
return {...rowData, ...auth.activeTenantData.extraModules.includes("agriculture") ? {agriculture: {}} : {}}
}
if (mode === "pagebreak") {
return {
id: uuidv4(),
mode: "pagebreak",
linkedEntitys: []
}
}
if (mode === "title" || mode === "subtitle") {
return {
id: uuidv4(),
mode: "title",
titleLevel: mode === "subtitle" ? 2 : 1,
linkedEntitys: []
}
}
if (mode === "text") {
return {
id: uuidv4(),
mode: "text",
linkedEntitys: []
}
}
return null
}
const getPositionAddMenuItems = (row) => [
positionAddOptions.map(option => ({
...option,
onSelect: () => addPosition(option.mode, row?.id)
}))
]
const toggleTitleSection = (rowId) => {
const nextCollapsedTitleRowIds = new Set(collapsedTitleRowIds.value)
if (nextCollapsedTitleRowIds.has(rowId)) {
nextCollapsedTitleRowIds.delete(rowId)
} else {
nextCollapsedTitleRowIds.add(rowId)
}
collapsedTitleRowIds.value = nextCollapsedTitleRowIds
}
const hiddenDocumentRowIds = computed(() => {
const hiddenRowIds = new Set()
const collapsedTitleLevels = []
itemInfo.value.rows.forEach((row) => {
if (row.mode === 'title') {
const titleLevel = Math.min(Math.max(Number(row.titleLevel) || 1, 1), 3)
while (
collapsedTitleLevels.length > 0
&& collapsedTitleLevels[collapsedTitleLevels.length - 1] >= titleLevel
) {
collapsedTitleLevels.pop()
}
if (collapsedTitleLevels.length > 0) hiddenRowIds.add(row.id)
if (collapsedTitleRowIds.value.has(row.id)) collapsedTitleLevels.push(titleLevel)
} else if (collapsedTitleLevels.length > 0) {
hiddenRowIds.add(row.id)
}
})
return hiddenRowIds
})
const isDocumentRowHidden = (rowId) => hiddenDocumentRowIds.value.has(rowId)
const getFullWidthPositionColspan = () => deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type) ? 5 : 8
const getTitlePositionColspan = () => deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type) ? 4 : 7
const addPosition = (mode, afterRowId = null) => {
let taxPercentage = 19
if (['13b UStG', '19 UStG', '12.3 UStG'].includes(itemInfo.value.taxType)) {
taxPercentage = 0
}
const newRow = createPositionRow(mode, taxPercentage)
if (!newRow) return
const insertAfterIndex = afterRowId ? itemInfo.value.rows.findIndex(row => row.id === afterRowId) : -1
if (insertAfterIndex >= 0) {
itemInfo.value.rows.splice(insertAfterIndex + 1, 0, newRow)
} else {
itemInfo.value.rows.push(newRow)
}
setPosNumbers()
}
const removePosition = (id) => {
itemInfo.value.rows = itemInfo.value.rows.filter(row => row.id !== id)
setPosNumbers()
}
const normalizeZip = (value) => String(value || "").replace(/\D/g, "")
const sanitizeAddressZipInput = () => {
itemInfo.value.address.zip = normalizeZip(itemInfo.value.address.zip)
}
const checkAddressZip = async () => {
const zip = normalizeZip(itemInfo.value.address.zip)
itemInfo.value.address.zip = zip
if (![4, 5].includes(zip.length)) return
const zipData = await useFunctions().useZipCheck(zip)
if (zipData?.zip) {
itemInfo.value.address.zip = zipData.zip
}
if (zipData?.short) {
itemInfo.value.address.city = zipData.short
}
}
const findDocumentErrors = computed(() => {
let errors = []
if (itemInfo.value.customer === null) errors.push({message: "Es ist kein Kunde ausgewählt", type: "breaking"})
if (itemInfo.value.contact === null) errors.push({message: "Es ist kein Kontakt ausgewählt", type: "info"})
if (itemInfo.value.letterhead === null) errors.push({message: "Es ist kein Briefpapier ausgewählt", type: "breaking"})
if (itemInfo.value.created_by === null || !itemInfo.value.created_by) errors.push({message: "Es ist kein Mitarbeiter ausgewählt", type: "breaking"})
if (!itemInfo.value.address.street) errors.push({
message: "Es ist keine Straße im Adressat angegeben",
type: "breaking"
})
if (!itemInfo.value.address.zip) errors.push({
message: "Es ist keine Postleitzahl im Adressat angegeben",
type: "breaking"
})
if (!itemInfo.value.address.city) errors.push({
message: "Es ist keine Stadt im Adressat angegeben",
type: "breaking"
})
if (itemInfo.value.project === null) errors.push({message: "Es ist kein Projekt ausgewählt", type: "info"})
if (itemInfo.value.plant === null) errors.push({message: "Es ist kein Objekt ausgewählt", type: "info"})
if (itemInfo.value.contract === null) errors.push({message: "Es ist kein Vertrag ausgewählt", type: "info"})
if (['Lieferzeitraum', 'Leistungszeitraum'].includes(itemInfo.value.deliveryDateType) && itemInfo.value.type !== "serialInvoices") {
if (itemInfo.value.deliveryDateEnd === null) errors.push({
message: `Es ist kein Enddatum für den ${itemInfo.value.deliveryDateType} angegeben`,
type: "breaking"
})
}
if (itemInfo.value.rows.length === 0) {
errors.push({message: "Es sind keine Positionen angegeben", type: "breaking"})
} else {
itemInfo.value.rows.forEach((row,index) => {
if (!quoteLikeDocumentTypes.includes(itemInfo.value.type) && row.optional) {
errors.push({
message: `Position ${row.pos} ist als Optional markiert. Dies wird nur in Angeboten und Kostenschätzungen unterstützt.`,
type: "breaking"
})
}
if (!quoteLikeDocumentTypes.includes(itemInfo.value.type) && row.alternative) {
errors.push({
message: `Position ${row.pos} ist als Alternativ markiert. Dies wird nur in Angeboten und Kostenschätzungen unterstützt.`,
type: "breaking"
})
}
if (row.mode === "normal" && !row.product) errors.push({
message: `In Position ${row.pos} ist kein Artikel ausgewählt`,
type: "breaking"
})
if (row.mode === "service" && !row.service) errors.push({
message: `In Position ${row.pos} ist keine Leistung ausgewählt`,
type: "breaking"
})
if (row.mode === "title" && !row.text) errors.push({
message: `In Position ${row.pos} ist kein Titel hinterlegt`,
type: "breaking"
})
//if(row.mode === "text" && !row.text) errors.push({message: `In einer Freitext Position ist kein Titel hinterlegt`, type: "breaking"})
if (row.mode === "free" && !row.text) errors.push({
message: `In einer freien Position ist kein Titel hinterlegt`,
type: "breaking"
})
if (["normal", "service", "free"].includes(row.mode)) {
if (!row.taxPercent && typeof row.taxPercent !== "number" && !deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type)) errors.push({
message: `In Position ${row.pos} ist kein Steuersatz hinterlegt`,
type: "breaking"
})
if (!row.price && typeof row.price !== "number" && !deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type)) errors.push({
message: `In Position ${row.pos} ist kein Preis hinterlegt`,
type: "breaking"
})
if (!row.unit) errors.push({message: `In Position ${row.pos} ist keine Einheit hinterlegt`, type: "breaking"})
}
if (row.agriculture) {
if (row.agriculture.dieselUsage && (!row.agriculture.dieselPrice && typeof row.agriculture.dieselPrice !== "number")) {
errors.push({message: `In Position ${row.pos} ist kein Dieselpreis hinterlegt`, type: "breaking"})
} else if (row.agriculture.dieselUsage && row.agriculture.dieselPrice === 0) {
errors.push({message: `In Position ${row.pos} ist 0,00 € als Dieselpreis hinterlegt`, type: "info"})
}
}
if (index === itemInfo.value.rows.length - 1 && row.mode === "pagebreak") {
errors.push({message: `Die letze Position darf kein Seitenumbruch sein`, type: "breaking"})
}
})
}
if (itemInfo.value.type === "serialInvoices") {
if (!itemInfo.value.serialConfig.intervall) errors.push({
message: `Kein Intervall für die Ausführung festgelegt`,
type: "breaking"
})
if (!itemInfo.value.serialConfig.dateDirection) errors.push({
message: `Kein Richtung für die Datierung festgelegt`,
type: "breaking"
})
if (!itemInfo.value.serialConfig.firstExecution) errors.push({
message: `Kein Datum für die erste Ausführung festgelegt`,
type: "breaking"
})
if (!itemInfo.value.serialConfig.executionUntil) errors.push({
message: `Kein Datum für die letzte Ausführung festgelegt`,
type: "info"
})
}
return errors.sort((a) => (a.type === "breaking") ? -1 : 1)
})
const tabItems = computed(() => {
return [
{
label: "Editor"
},
{
label: "Vorschau",
disabled: findDocumentErrors.value.filter(i => i.type === 'breaking').length > 0
}
]
})
const renderCurrency = (value, currency = "€") => {
//return Number(value).toFixed(2).replace(".",",") + " " + currency
return useCurrency(value, currency)
}
const documentTotal = computed(() => {
let totalNet = 0
let total19 = 0
let totalNet19 = 0
let total7 = 0
let totalNet7 = 0
let total0 = 0
let totalNet0 = 0
itemInfo.value.rows.filter(i => !i.optional && !i.alternative).forEach(row => {
if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100)).toFixed(3)
totalNet = totalNet + Number(rowPrice)
if (row.taxPercent === 19) {
total19 = total19 + Number(rowPrice * 0.19)
totalNet19 += Number(rowPrice)
} else if (row.taxPercent === 7) {
total7 = total7 + Number(rowPrice * 0.07)
totalNet7 += Number(rowPrice)
} else if (row.taxPercent === 0) {
totalNet0 += Number(rowPrice)
}
}
})
//Title Sum
let titleSums = {}
let titleSumsTransfer = {}
let activeTitleKeys = []
let transferCounter = 0
itemInfo.value.rows.forEach(row => {
if (row.mode === 'title') {
let title = `${row.pos} - ${row.text}`
const titleLevel = Math.min(Math.max(Number(row.titleLevel) || 1, 1), 3)
titleSums[title] = 0
if (activeTitleKeys.some(Boolean)) {
titleSumsTransfer[title] = transferCounter
}
activeTitleKeys = activeTitleKeys.slice(0, titleLevel - 1)
activeTitleKeys[titleLevel - 1] = title
activeTitleKeys = activeTitleKeys.slice(0, titleLevel)
} else if (!['pagebreak', 'text'].includes(row.mode) && activeTitleKeys.some(Boolean) && !row.optional && !row.alternative) {
const rowSum = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100))
activeTitleKeys.forEach(titleKey => {
titleSums[titleKey] = Number(titleSums[titleKey]) + rowSum
})
transferCounter += rowSum
console.log(transferCounter)
}
})
console.log(titleSumsTransfer)
let totalGross = Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2))
let totalGrossAlreadyPaid = 0
itemInfo.value.usedAdvanceInvoices.forEach(advanceInvoiceId => {
let advanceInvoice = createddocuments.value.find(i => i.id === advanceInvoiceId)
let priceNet = advanceInvoice.rows.find(i => i.advanceInvoiceData).price
let partSum = priceNet * ((100 + advanceInvoice.rows.find(i => i.advanceInvoiceData).taxPercent) / 100)
totalGrossAlreadyPaid += partSum
})
console.log(totalGrossAlreadyPaid)
let sumToPay = 0
if (itemInfo.value.type === "invoices") {
sumToPay = totalGross - totalGrossAlreadyPaid
} else if (itemInfo.value.type === "cancellationInvoices") {
sumToPay = totalGross + totalGrossAlreadyPaid
}
return {
titleSums: titleSums,
titleSumsTransfer: titleSumsTransfer,
totalNet: totalNet,
total19: total19,
totalNet19: totalNet19,
total7: total7,
totalNet7: totalNet7,
total0: total0,
totalNet0: totalNet0,
totalGross: totalGross,
totalGrossAlreadyPaid: totalGrossAlreadyPaid,
totalSumToPay: sumToPay
}
})
const documentReport = computed(() => {
let totalProductsPurchasePrice = 0
let totalProductsFromServicesPurchasePrice = 0
let totalHoursFromServices = {
total: 0,
totalPurchasePrice: 0,
byName: {}
}
let totalHoursSellingPrice = 0
itemInfo.value.rows.filter(i => !i.optional && !i.alternative).forEach(row => {
if (row.product) {
let product = products.value.find(i => i.id === row.product)
totalProductsPurchasePrice += (product?.purchase_price || 0) * row.quantity
} else if (row.mode === "free") {
totalProductsPurchasePrice += Number(row.purchasePrice || 0) * Number(row.quantity || 0)
} else if (row.service) {
let service = services.value.find(i => i.id === row.service)
if (!service) return
if (service.materialComposition) {
service.materialComposition.forEach(entry => {
let productData = products.value.find(i => i.id === entry.product)
totalProductsFromServicesPurchasePrice += (productData?.purchase_price || 0) * entry.quantity * row.quantity
})
}
if (service.personalComposition) {
service.personalComposition.forEach(entry => {
totalHoursFromServices.total += entry.quantity * row.quantity
totalHoursFromServices.totalPurchasePrice += entry.quantity * entry.purchasePrice * row.quantity
totalHoursSellingPrice += entry.quantity * entry.price * row.quantity
if (totalHoursFromServices.byName[entry.name]) {
totalHoursFromServices.byName[entry.name] += entry.quantity * row.quantity
} else {
totalHoursFromServices.byName[entry.name] = entry.quantity * row.quantity
}
})
} else {
//totalHoursSellingPrice += service.sellingPriceComposed.totalWorker * row.quantity
}
//totalProductsPurchasePrice += product.purchasePrice * row.quantity
}
})
let totalMargin = documentTotal.value.totalNet - totalProductsPurchasePrice - totalProductsFromServicesPurchasePrice - totalHoursFromServices.totalPurchasePrice
return {
totalProductsPurchasePrice,
totalProductsFromServicesPurchasePrice,
totalMargin,
totalHoursFromServices,
totalHoursSellingPrice,
}
})
const processDieselPosition = () => {
let agricultureData = {
dieselUsageTotal: 0,
dieselPriceTotal: 0,
adblueUsageTotal: 0,
adbluePriceTotal: 0
}
itemInfo.value.rows.forEach(row => {
if (row.agriculture && row.agriculture.dieselUsage) {
console.log(row.agriculture)
agricultureData.dieselUsageTotal += Number(row.agriculture.dieselUsage)
agricultureData.dieselPriceTotal += Number(row.agriculture.dieselPrice || 0) * Number(row.agriculture.dieselUsage)
agricultureData.adblueUsageTotal += Number(row.agriculture.adblueUsage || 0)
agricultureData.adbluePriceTotal += Number(row.agriculture.adbluePrice || 0) * Number(row.agriculture.adblueUsage || 0)
}
})
console.log(agricultureData)
if (agricultureData.dieselUsageTotal !== 0) {
if (itemInfo.value.rows.find(i => i.key === "dieselPos")) {
let existingIndex = itemInfo.value.rows.findIndex(i => i.key === "dieselPos")
itemInfo.value.rows[existingIndex] = {
...itemInfo.value.rows[existingIndex],
price: agricultureData.dieselPriceTotal,
text: `${agricultureData.dieselUsageTotal} L Diesel`,
}
let existingAdblueIndex = itemInfo.value.rows.findIndex(i => i.key === "adbluePos")
itemInfo.value.rows[existingAdblueIndex] = {
...itemInfo.value.rows[existingAdblueIndex],
price: agricultureData.adbluePriceTotal,
text: `${agricultureData.adblueUsageTotal} L AdBlue`,
}
} else {
itemInfo.value.rows.push({
mode: "free",
text: `${(agricultureData.dieselUsageTotal).toFixed(2).replace(".", ",")} L Diesel`,
quantity: 1,
unit: 10,
price: agricultureData.dieselPriceTotal,
taxPercent: 19,
discountPercent: 0,
key: "dieselPos"
})
itemInfo.value.rows.push({
mode: "free",
text: `${(agricultureData.adblueUsageTotal).toFixed(2).replace(".", ",")} L AdBlue`,
quantity: 1,
unit: 10,
price: agricultureData.adbluePriceTotal,
taxPercent: 19,
discountPercent: 0,
key: "adbluePos"
})
setPosNumbers()
}
}
itemInfo.value.agriculture = {...itemInfo.value.agriculture, ...agricultureData}
}
const getDocumentData = async () => {
let customerData = customers.value.find(i => i.id === itemInfo.value.customer)
let contactData = contacts.value.find(i => i.id === itemInfo.value.contact)
let businessInfo = auth.activeTenantData.businessInfo || {}
const paymentQrBankAccount = bankaccounts.value.find(account => !account.archived && !account.expired && account.iban)
const paymentQrIban = businessInfo.iban || paymentQrBankAccount?.iban
const paymentQrBic = businessInfo.bic || paymentQrBankAccount?.bic
const paymentQrRecipient = businessInfo.bankAccountOwner || paymentQrBankAccount?.ownerName || businessInfo.name || auth.activeTenantData.name
if (auth.activeTenantData.extraModules.includes("agriculture")) {
itemInfo.value.rows.forEach(row => {
if (row.agriculture && row.agriculture.dieselUsage) {
row.agriculture.description = `${row.agriculture.dieselUsage} L Diesel zu ${renderCurrency(row.agriculture.dieselPrice)}/L verbraucht ${row.description ? "\n" + row.description : ""}`
}
})
}
let rows = itemInfo.value.rows
if (itemInfo.value.taxType === "13b UStG" || itemInfo.value.taxType === "19 UStG") {
rows = rows.map(row => {
return {
...row,
taxPercent: 0
}
})
}
rows = itemInfo.value.rows.map(row => {
let unit = units.value.find(i => i.id === row.unit)
if (!['pagebreak', 'title'].includes(row.mode)) {
if (row.agriculture && row.agriculture.description) {
console.log("Row has Agri")
row.descriptionText = row.agriculture.description
} else if (row.description) {
console.log("Row has no Agri")
row.descriptionText = row.description
} else {
delete row.descriptionText
}
}
if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
if (row.mode === 'normal') row.text = products.value.find(i => i.id === row.product)?.name || ''
if (row.mode === 'service') row.text = services.value.find(i => i.id === row.service)?.name || ''
return {
...row,
rowAmount: renderCurrency(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) / 100)),
quantity: String(row.quantity).replace(".", ","),
unit: unit.short,
pos: String(row.pos),
price: renderCurrency(row.price),
discountText: `(Rabatt: ${row.discountPercent} %)`
}
} else {
return row
}
})
//Compile Start & EndText
const templateStartText = documentTemplateHandlebars.compile(itemInfo.value.startText);
const templateEndText = documentTemplateHandlebars.compile(itemInfo.value.endText);
const generateContext = (itemInfo, contactData) => {
return {
lohnkosten: documentReport.value.totalHoursSellingPrice ? useCurrency(documentReport.value.totalHoursSellingPrice) : null,
anrede: (contactData && contactData.salutation) || (customerData && customerData.salutation),
titel: (contactData && contactData.title) || (customerData && customerData.title),
vorname: (contactData && contactData.firstName) || (customerData && customerData.firstname),
nachname: (contactData && contactData.lastName) || (customerData && customerData.lastname),
kundenname: customerData && customerData.name,
zahlungsziel_in_tagen: itemInfo.paymentDays,
zahlungsart: itemInfo.payment_type === "transfer" ? "Überweisung" : "Lastschrift",
diesel_gesamtverbrauch: (itemInfo.agriculture && itemInfo.agriculture.dieselUsageTotal) && itemInfo.agriculture.dieselUsageTotal
}
}
let contactPerson = (await useNuxtApp().$api(`/api/user/${itemInfo.value.created_by}`, {
method: "GET"
})).profile
let returnTitleSums = {}
if (Object.keys(documentTotal.value.titleSums).length > 0) {
Object.keys(documentTotal.value.titleSums).forEach(key => {
returnTitleSums[key] = renderCurrency(documentTotal.value.titleSums[key])
})
}
let returnTitleSumsTransfer = {}
if (Object.keys(documentTotal.value.titleSumsTransfer).length > 0) {
Object.keys(documentTotal.value.titleSumsTransfer).forEach(key => {
returnTitleSumsTransfer[key] = renderCurrency(documentTotal.value.titleSumsTransfer[key])
})
}
console.log(returnTitleSums)
console.log(returnTitleSumsTransfer)
const recipientCountry = String(itemInfo.value.address.country ?? customerData.infoData?.country ?? "").trim()
const returnData = {
type: itemInfo.value.type,
taxType: itemInfo.value.taxType,
adressLine: `${businessInfo.name}, ${businessInfo.street}, ${businessInfo.zip} ${businessInfo.city}`,
/*recipient: {
name: customerData.name,
contact: contactData ? `${contactData.firstName} ${contactData.lastName}` : "",
street: itemInfo.value.address.street || customerData.infoData.street,
special: itemInfo.value.address.special || customerData.infoData.special,
city: itemInfo.value.address.city || customerData.infoData.city,
zip: itemInfo.value.address.zip || customerData.infoData.zip
},*/
recipient: [
customerData.name,
...customerData.nameAddition ? [customerData.nameAddition] : [],
...contactData ? [`${contactData.firstName} ${contactData.lastName}`] : [],
itemInfo.value.address.street,
...itemInfo.value.address.special ? [itemInfo.value.address.special] : [],
`${itemInfo.value.address.zip} ${itemInfo.value.address.city}`,
...(recipientCountry && recipientCountry.toLocaleLowerCase("de") !== "deutschland" ? [recipientCountry] : []),
],
/*info: {
customerNumber: customerData.customerNumber,
documentNumber: itemInfo.value.documentNumber,
documentNumberTitle: itemInfo.value.documentNumberTitle,
documentDate: dayjs(itemInfo.value.documentDate).format("DD.MM.YYYY"),
deliveryDate: dayjs(itemInfo.value.deliveryDate).format("DD.MM.YYYY"),
deliveryDateEnd: itemInfo.value.deliveryDateEnd ? dayjs(itemInfo.value.deliveryDateEnd).format("DD.MM.YYYY") : null,
deliveryDateType: itemInfo.value.deliveryDateType,
contactPerson: contactPerson.fullName,
contactTel: contactPerson.fixedTel || contactPerson.mobileTel,
contactEMail: contactPerson.email,
project: projects.value.find(i => i.id === itemInfo.value.project) ? projects.value.find(i => i.id === itemInfo.value.project).name : null,
plant: plants.value.find(i => i.id === itemInfo.value.plant) ? plants.value.find(i => i.id === itemInfo.value.plants).name : null,
},*/
info: [
{
label: itemInfo.value.documentNumberTitle,
content: itemInfo.value.documentNumber || "XXXX",
}, {
label: "Kundennummer",
content: formatNumberLikeValue(customerData.customerNumber),
}, {
label: "Belegdatum",
content: itemInfo.value.documentDate ? dayjs(itemInfo.value.documentDate).format("DD.MM.YYYY") : "XXXX",
},
...itemInfo.value.deliveryDateType !== "Kein Lieferdatum anzeigen" ? [{
label: itemInfo.value.deliveryDateType,
content: !['Lieferzeitraum', 'Leistungszeitraum'].includes(itemInfo.value.deliveryDateType) ? (itemInfo.value.deliveryDate ? dayjs(itemInfo.value.deliveryDate).format("DD.MM.YYYY") : "XXXX") : `${itemInfo.value.deliveryDate ? dayjs(itemInfo.value.deliveryDate).format("DD.MM.YYYY") : "XXXX"} - ${itemInfo.value.deliveryDateEnd ? dayjs(itemInfo.value.deliveryDateEnd).format("DD.MM.YYYY") : "XXXX"}`,
}] : [],
/*{
label: itemInfo.value.deliveryDateType,
content: !['Lieferzeitraum','Leistungszeitraum'].includes(itemInfo.value.deliveryDateType) ? dayjs(itemInfo.value.deliveryDate).format("DD.MM.YYYY") : `${dayjs(itemInfo.value.deliveryDate).format("DD.MM.YYYY")} - ${dayjs(itemInfo.value.deliveryDateEnd).format("DD.MM.YYYY")}`,
},*/
{
label: "Ansprechpartner",
content: contactPerson.full_name,
},
...itemInfo.value.contactTel || contactPerson.fixed_tel || contactPerson.mobile_tel ? [{
label: "Telefon",
content: itemInfo.value.contactTel || contactPerson.fixed_tel || contactPerson.mobile_tel,
}] : [],
...contactPerson.email ? [{
label: "E-Mail",
content: contactPerson.email,
}] : [],
...itemInfo.value.plant ? [{
label: "Objekt",
content: getPlantName(itemInfo.value.plant),
}] : [],
...itemInfo.value.project ? [{
label: "Projekt",
content: getProjectName(itemInfo.value.project)
}] : [],
...itemInfo.value.contract ? [{
label: "Vertrag",
content: getContractNumber(itemInfo.value.contract)
}] : []
],
title: itemInfo.value.title,
description: itemInfo.value.description,
endText: templateEndText(generateContext(itemInfo.value, contactData)),
startText: templateStartText(generateContext(itemInfo.value, contactData)),
rows: rows,
totalArray: [
{
label: "Nettobetrag",
content: renderCurrency(documentTotal.value.totalNet),
},
...rows.find(i => i.taxPercent === 19) && !["13b UStG"].includes(itemInfo.value.taxType) ? [{
label: `zzgl. 19% USt auf ${renderCurrency(documentTotal.value.totalNet19)}`,
content: renderCurrency(documentTotal.value.total19),
}] : [],
...rows.find(i => i.taxPercent === 7) && !["13b UStG"].includes(itemInfo.value.taxType) ? [{
label: `zzgl. 7% USt auf ${renderCurrency(documentTotal.value.totalNet7)}`,
content: renderCurrency(documentTotal.value.total7),
}] : [],
...rows.find(i => i.taxPercent === 0) && !["13b UStG"].includes(itemInfo.value.taxType) ? [{
label: `zzgl. 0% USt auf ${renderCurrency(documentTotal.value.totalNet0)}`,
content: renderCurrency(documentTotal.value.total0),
}] : [],
{
label: "Gesamtbetrag",
content: renderCurrency(documentTotal.value.totalGross),
},
],
total: {
totalNet: renderCurrency(documentTotal.value.totalNet),
total19: renderCurrency(documentTotal.value.total19),
total0: renderCurrency(documentTotal.value.total0),
totalGross: renderCurrency(documentTotal.value.totalGross),
totalGrossAlreadyPaid: renderCurrency(documentTotal.value.totalGrossAlreadyPaid),
totalSumToPay: renderCurrency(documentTotal.value.totalSumToPay),
titleSums: returnTitleSums,
titleSumsTransfer: returnTitleSumsTransfer
},
agriculture: itemInfo.value.agriculture,
usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices.map(i => {
return createddocuments.value.find(x => x.id === i)
}),
paymentQr: paymentQrIban && itemInfo.value.payment_type === "transfer" ? {
iban: paymentQrIban,
bic: paymentQrBic,
recipientName: paymentQrRecipient,
amount: documentTotal.value.totalSumToPay,
remittance: itemInfo.value.documentNumber || itemInfo.value.title,
} : null
}
console.log(returnData)
return returnData
}
const showDocument = ref(false)
const uri = ref("")
const generateDocument = async () => {
showDocument.value = false
const selectedLetterhead = getSelectedLetterhead()
if (!selectedLetterhead?.path) {
toast.add({ title: "Briefpapier fehlt", color: "error" })
return
}
const path = selectedLetterhead.path
uri.value = await useFunctions().useCreatePDF(await getDocumentData(), path, "createdDocument")
/*uri.value = await useNuxtApp().$api("/api/functions/createinvoicepdf",{
method: "POST",
body: {
invoiceData: await getDocumentData(),
backgroundPath: path
}
})*/
showDocument.value = true
}
const onChangeTab = (index) => {
selectedTab.value = String(index)
if (String(index) === "1") {
generateDocument()
}
}
const setPosNumbers = () => {
let titleCounters = [0, 0, 0]
let positionIndex = 1
let currentTitlePath = []
const hasTitles = itemInfo.value.rows.some(row => row.mode === "title")
let rows = itemInfo.value.rows.map(row => {
if (row.mode === 'title') {
const titleLevel = Math.min(Math.max(Number(row.titleLevel) || 1, 1), titleCounters.length)
row.titleLevel = titleLevel
for (let index = 0; index < titleLevel - 1; index += 1) {
if (titleCounters[index] === 0) {
titleCounters[index] = 1
}
}
titleCounters[titleLevel - 1] += 1
for (let index = titleLevel; index < titleCounters.length; index += 1) {
titleCounters[index] = 0
}
currentTitlePath = titleCounters.slice(0, titleLevel)
row.pos = currentTitlePath.join(".")
positionIndex = 1
} else if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
row.pos = hasTitles && currentTitlePath.length > 0 ? `${currentTitlePath.join(".")}.${positionIndex}` : `${positionIndex}`
positionIndex += 1
}
if (!row.id) {
row.id = uuidv4()
}
return row
})
}
const saveSerialInvoice = async () => {
let createData = {
type: itemInfo.value.type,
state: 'Erstellt',
customer: normalizeEntityId(itemInfo.value.customer),
contact: normalizeEntityId(itemInfo.value.contact),
contract: normalizeEntityId(itemInfo.value.contract),
address: itemInfo.value.address,
project: normalizeEntityId(itemInfo.value.project),
costcentre: itemInfo.value.costcentre,
paymentDays: itemInfo.value.paymentDays,
payment_type: itemInfo.value.payment_type,
outgoingsepamandate: normalizeEntityId(itemInfo.value.outgoingsepamandate),
deliveryDateType: "Leistungszeitraum",
createdBy: itemInfo.value.createdBy,
created_by: itemInfo.value.created_by,
title: itemInfo.value.title,
description: itemInfo.value.description,
startText: itemInfo.value.startText,
endText: itemInfo.value.endText,
rows: itemInfo.value.rows,
contactPerson: itemInfo.value.contactPerson,
serialConfig: itemInfo.value.serialConfig,
letterhead: itemInfo.value.letterhead,
taxType:itemInfo.value.taxType,
availableInPortal: itemInfo.value.availableInPortal
}
let data = null
if (route.params.id) {
data = await useEntities("createddocuments").update(route.params.id, createData)
} else {
data = await useEntities("createddocuments").create(createData)
}
await router.push(`/createDocument/edit/${data.id}`)
}
const serializeTemplateData = () => {
const data = JSON.parse(JSON.stringify(itemInfo.value))
;["id", "createdAt", "tenant", "documentNumber", "documentDate", "state", "customer", "contact", "address", "project", "costcentre", "createddocument", "availableInPortal", "archived", "statementallocations", "files", "linkedDocument", "createddocuments", "serialexecution", "createdBy", "created_by"].forEach((key) => delete data[key])
return data
}
const saveDocumentTemplate = async () => {
if (!templateName.value.trim()) {
toast.add({ title: "Vorlagenname fehlt", description: "Bitte einen Namen für die Vorlage vergeben.", color: "error" })
return
}
const payload = {
name: templateName.value.trim(),
documentType: itemInfo.value.type,
templateData: serializeTemplateData(),
default: false,
}
const template = route.query.templateId
? await useEntities("documenttemplates").update(route.query.templateId, payload, true)
: await useEntities("documenttemplates").create(payload)
toast.add({ title: "Vorlage gespeichert", color: "success" })
await router.push(`/createDocument/edit?mode=template&templateId=${template.id || route.query.templateId}`)
}
const saveDocument = async (state, resetup = false) => {
if (!validateAdvanceInvoiceCreation()) return
itemInfo.value.state = state
normalizeCustomSurchargePercentage()
if (state !== "Entwurf" && !itemInfo.value.documentNumber) {
console.log("???")
const type = itemInfo.value.type
try {
itemInfo.value.documentNumber = await useFunctions().useNextNumber(type) //data.usedNumber
setDocumentTypeConfig(false)
} catch {
console.log("Error Happend")
}
}
if (auth.activeTenantData.extraModules.includes("agriculture")) {
itemInfo.value.rows.forEach(row => {
if (row.agriculture && row.agriculture.dieselUsage) {
row.agriculture.description = `${row.agriculture.dieselUsage} L Diesel zu ${renderCurrency(row.agriculture.dieselPrice)}/L verbraucht ${row.description ? "\n" + row.description : ""}`
}
})
}
//Check if Agricultural Description is Present
itemInfo.value.rows = itemInfo.value.rows.map(row => {
let descriptionText = ""
if (row.agriculture && row.agriculture.description) {
descriptionText = row.agriculture.description
} else {
descriptionText = row.description ? row.description : null
}
return {
...row,
descriptionText: descriptionText
}
})
let createData = {
type: itemInfo.value.type,
taxType: ['invoices', 'cancellationInvoices', 'advanceInvoices', 'confirmationOrders', ...quoteLikeDocumentTypes].includes(itemInfo.value.type) ? normalizeTaxTypeValue(itemInfo.value.taxType) : null,
state: itemInfo.value.state || "Entwurf",
customer: normalizeEntityId(itemInfo.value.customer),
contact: normalizeEntityId(itemInfo.value.contact),
contract: normalizeEntityId(itemInfo.value.contract),
address: itemInfo.value.address,
project: normalizeEntityId(itemInfo.value.project),
costcentre: itemInfo.value.costcentre,
plant: normalizeEntityId(itemInfo.value.plant),
documentNumber: itemInfo.value.documentNumber,
documentDate: itemInfo.value.documentDate,
deliveryDate: itemInfo.value.deliveryDate,
deliveryDateEnd: itemInfo.value.deliveryDateEnd,
paymentDays: itemInfo.value.paymentDays,
payment_type: itemInfo.value.payment_type,
outgoingsepamandate: normalizeEntityId(itemInfo.value.outgoingsepamandate),
deliveryDateType: itemInfo.value.deliveryDateType,
info: {},
createdBy: itemInfo.value.createdBy,
created_by: itemInfo.value.created_by,
title: itemInfo.value.title,
description: itemInfo.value.description,
startText: itemInfo.value.startText,
endText: itemInfo.value.endText,
rows: itemInfo.value.rows,
contactPerson: itemInfo.value.contactPerson,
createddocument: normalizeEntityId(itemInfo.value.createddocument),
agriculture: itemInfo.value.agriculture,
letterhead: normalizeEntityId(itemInfo.value.letterhead),
usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices,
availableInPortal: itemInfo.value.availableInPortal,
customSurchargePercentage: itemInfo.value.customSurchargePercentage,
report: documentReport.value
}
if (route.params.id) {
//await dataStore.updateItem("createddocuments", {...createData, id: itemInfo.value.id})
await useEntities("createddocuments").update(itemInfo.value.id, {...createData, id: itemInfo.value.id})
} else {
const data = await useEntities("createddocuments").create(createData)
console.log(data)
await router.push(`/createDocument/edit/${data.id}`)
}
if (resetup) await setupPage()
}
const saveFinalizedCostCentres = async () => {
const rowCostCentres = itemInfo.value.rows
.filter(row => row?.id)
.map(row => ({
rowId: String(row.id),
costcentre: row.costCentre || null,
}))
const result = await $api(`/api/functions/outgoing-documents/${itemInfo.value.id}/cost-centres`, {
method: "PUT",
body: {
costcentre: itemInfo.value.costcentre || null,
rowCostCentres,
},
})
itemInfo.value.costcentre = result.document.costcentre
itemInfo.value.rows = normalizeCreatedDocumentRows(result.document.rows)
toast.add({
title: "Kostenstellen gespeichert",
description: "Die Kostenstellen des fertiggestellten Belegs wurden aktualisiert.",
color: "success",
})
}
const selectedTab = ref("0")
const closeDocument = async () => {
if(selectedTab.value === "0") {
await generateDocument()
selectedTab.value = "1"
} else {
loaded.value = false
await saveDocument("Gebucht")
await uploadGeneratedDocument()
guard.value = false
await router.push(`/createDocument/show/${itemInfo.value.id}`)
}
}
// Erzeugt die PDF aus dem aktuellen Stand und hängt sie an den Beleg an, ohne den Datensatz zu speichern
const uploadGeneratedDocument = async () => {
await generateDocument()
if (!uri.value) throw new Error("PDF konnte nicht erzeugt werden")
let fileData = {}
fileData.project = itemInfo.value.project
fileData.createddocument = itemInfo.value.id
let mappedType = documentStorageFallbackTypes[itemInfo.value.type] || itemInfo.value.type
const folders = await useEntities("folders").select()
console.log(folders)
fileData.folder = folders.find(i => i.function === mappedType && i.year === Number(dayjs().format("YYYY"))).id
const tags = await useEntities("filetags").select()
console.log(tags)
fileData.type = tags.find(i => i.createdDocumentType === mappedType).id
function dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[arr.length - 1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type: mime});
}
let file = dataURLtoFile(uri.value, `${itemInfo.value.documentNumber}.pdf`)
await useFiles().uploadFiles(fileData, [file])
}
const regenerateMissingPdf = async () => {
guard.value = false
const showPath = `/createDocument/show/${route.params.id}`
try {
const document = await useEntities("createddocuments").selectSingle(route.params.id, "*,files(*)")
if (document?.state === "Entwurf" || document?.type === "serialInvoices") {
toast.add({ title: "PDF kann nur für fertiggestellte Belege nacherzeugt werden", color: "error" })
} else if ((document?.files || []).length > 0) {
toast.add({ title: "Am Beleg ist bereits ein Dokument angehängt", color: "warning" })
} else {
// Vertrags- und Mandats-Watcher überschreiben beim Laden die Zahlungsdaten, für die PDF gilt der gespeicherte Stand
const restoreStoredPaymentData = () => {
itemInfo.value.payment_type = document.payment_type
itemInfo.value.outgoingsepamandate = normalizeEntityId(document.outgoingsepamandate)
}
await nextTick()
restoreStoredPaymentData()
await nextTick()
restoreStoredPaymentData()
await uploadGeneratedDocument()
toast.add({ title: "PDF nacherzeugt", color: "success" })
}
} catch (error) {
console.error(error)
toast.add({ title: "PDF konnte nicht nacherzeugt werden", description: error?.data?.error || error?.message, color: "error" })
}
await router.replace(showPath)
}
const getTextTemplateByType = (type, pos) => {
const finalType = textTemplateFallbackTypes[type] || type
if (pos) {
return texttemplates.value.filter(i => i.documentType === finalType && i.pos === pos)
} else {
return texttemplates.value.filter(i => i.documentType === finalType)
}
}
const checkCompatibilityWithInputPrice = () => {
itemInfo.value.rows = normalizeCreatedDocumentRows(itemInfo.value.rows)
itemInfo.value.rows.forEach(row => {
if (!row.inputPrice) {
row.inputPrice = row.price
}
})
}
const normalizeCustomSurchargePercentage = () => {
const percentage = Number(itemInfo.value.customSurchargePercentage)
itemInfo.value.customSurchargePercentage = Number.isFinite(percentage) ? percentage : 0
}
const updateCustomSurcharge = () => {
normalizeCustomSurchargePercentage()
itemInfo.value.rows.forEach(row => {
if (!["pagebreak", "title", "text"].includes(row.mode) && isImportedAdvanceInvoiceRow(row)) {
row.price = Number(row.inputPrice || 0)
} else if (!["pagebreak", "title", "text"].includes(row.mode)) {
//setRowData(row)
row.price = Number((row.inputPrice * (1 + itemInfo.value.customSurchargePercentage / 100)).toFixed(2))
}
})
}
const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {}) => {
console.log("Set Row Data")
if (service && service.id) {
row.service = service.id
services.value = await useEntities("services").select("*")
}
if (product && product.id) {
row.product = product.id
products.value = await useEntities("products").select("*")
}
if (row.service) {
console.log(service)
row.unit = normalizeUnitId(service?.unit ?? services.value.find(i => i.id === row.service)?.unit)
row.inputPrice = ((service.sellingPriceComposed.total || service.sellingPrice) ? (service.sellingPriceComposed.total || service.sellingPrice) : (services.value.find(i => i.id === row.service).sellingPriceComposed.total || services.value.find(i => i.id === row.service).sellingPrice))
row.description = service.description ? service.description : (services.value.find(i => i.id === row.service) ? services.value.find(i => i.id === row.service).description : "")
row.costCentre = service.costcentre || services.value.find(i => i.id === row.service)?.costcentre || itemInfo.value.costcentre
if (['13b UStG', '19 UStG'].includes(itemInfo.value.taxType)) {
row.taxPercent = 0
} else {
row.taxPercent = service.taxPercentage ? service.taxPercentage : services.value.find(i => i.id === row.service).taxPercentage
}
}
if (row.product) {
console.log("Product Detected")
row.unit = normalizeUnitId(product?.unit ?? products.value.find(i => i.id === row.product)?.unit)
row.inputPrice = (product.selling_price ? product.selling_price : products.value.find(i => i.id === row.product).selling_price)
row.costCentre = row.costCentre || itemInfo.value.costcentre
//row.price = Number((row.originalPrice * (1 + itemInfo.value.customSurchargePercentage /100)).toFixed(2))
row.description = product.description ? product.description : (products.value.find(i => i.id === row.product) ? products.value.find(i => i.id === row.product).description : "")
if (['13b UStG', '19 UStG'].includes(itemInfo.value.taxType)) {
row.taxPercent = 0
} else {
row.taxPercent = product.tax_percentage ? product.tax_percentage : products.value.find(i => i.id === row.product).tax_percentage
}
}
updateCustomSurcharge()
}
</script>
<template>
<UDashboardNavbar>
<template #left>
<UButton
icon="i-heroicons-chevron-left"
variant="outline"
@click="router.back()/*router.push(`/standardEntity/${type}`)*/"
>
<!-- {{dataType.label}}-->
</UButton>
</template>
<template #right>
<ArchiveButton
color="error"
type="createddocuments"
v-if="itemInfo.id && !isTemplateMode && (itemInfo.state === 'Entwurf' || itemInfo.type === 'serialInvoices')"
variant="outline"
@confirmed="useEntities('createddocuments').update(itemInfo.id,{archived: true}),
router.push('/')"
/>
<UButton
icon="i-mdi-content-save"
@click="saveDocument('Entwurf',true)"
v-if="itemInfo.type !== 'serialInvoices' && !isTemplateMode && itemInfo.state === 'Entwurf'"
:disabled="!itemInfo.customer"
>
Speichern
</UButton>
<UButton
v-if="isTemplateMode"
icon="i-mdi-content-save"
color="primary"
@click="saveDocumentTemplate"
>
Als Vorlage speichern
</UButton>
<UButton
@click="closeDocument"
v-if="itemInfo.id && itemInfo.type !== 'serialInvoices' && !isTemplateMode && itemInfo.state === 'Entwurf'"
>
{{selectedTab === '0' ? "Vorschau zeigen" : "Fertigstellen"}}
</UButton>
<UButton
icon="i-heroicons-tag"
@click="saveFinalizedCostCentres"
v-if="itemInfo.id && itemInfo.type !== 'serialInvoices' && !isTemplateMode && itemInfo.state === 'Gebucht'"
>
Kostenstellen speichern
</UButton>
<UButton
icon="i-mdi-content-save"
@click="saveSerialInvoice"
v-if="itemInfo.type === 'serialInvoices' && !isTemplateMode"
>
Serienrechnung
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<div v-if="isTemplateMode" class="px-5 pt-5">
<UAlert
color="primary"
variant="soft"
title="Vorlagenmodus"
description="Dieser Editor speichert ausschließlich eine Dokumentenvorlage. Es wird kein Ausgangsbeleg erstellt."
/>
<UFormField label="Vorlagenname" required class="mt-3">
<UInput v-model="templateName" placeholder="z. B. Standardrechnung" class="w-full" />
</UFormField>
</div>
<UTabs class="p-5" :items="tabItems" @update:model-value="onChangeTab" v-if="loaded" v-model="selectedTab">
<template #content="{item}">
<div v-if="item.label === 'Editor'">
<UAlert
class="my-5"
title="Vorhandene Probleme und Informationen:"
:color="findDocumentErrors.filter(i => i.type === 'breaking').length > 0 ? 'error' : 'white'"
variant="outline"
v-if="findDocumentErrors.length > 0"
>
<template #description>
<ul class="list-disc ml-5">
<li v-for="error in findDocumentErrors"
:class="[...error.type === 'breaking' ? ['text-rose-600'] : ['dark:text-white','text-black']]">
{{ error.message }}
</li>
</ul>
</template>
</UAlert>
<InputGroup class="w-full items-start">
<div class="w-1/3 min-w-0 mr-5">
<UFormField
label="Dokumenttyp:"
>
<InputGroup>
<USelectMenu
:items="documentTypeItems"
v-model="itemInfo.type"
:disabled="isTemplateMode"
value-key="type"
label-key="label"
@update:model-value="setDocumentTypeConfig"
class="w-full min-w-0"
>
<template #default>
{{ dataStore.documentTypesForCreation[itemInfo.type].labelSingle }}
</template>
</USelectMenu>
<!-- <UButton
variant="outline"
v-if="itemInfo.type === 'advanceInvoices'"
icon="i-heroicons-arrow-down-tray"
@click="showAdvanceInvoiceCalcModal = true"
>Auto Positionen</UButton>-->
</InputGroup>
<USlideover
v-model:open="showAdvanceInvoiceCalcModal"
>
<template #body>
<UCard class="h-full">
<template #header>
<UButton @click="importPositions">Übernehmen</UButton>
</template>
<UFormField
label="Gesamtsumme:"
>
<UInput
type="number"
:step="0.01"
v-model="advanceInvoiceData.totalSumNet"
@focusout="advanceInvoiceData.part = advanceInvoiceData.totalSumNet / 100 * advanceInvoiceData.partPerPecentage"
/>
</UFormField>
<UFormField
label="Prozent:"
>
<UInput
type="number"
:step="0.01"
v-model="advanceInvoiceData.partPerPecentage"
@focusout="advanceInvoiceData.part = advanceInvoiceData.totalSumNet / 100 * advanceInvoiceData.partPerPecentage"
/>
</UFormField>
<UFormField
label="Abzurechnender Anteil:"
>
<UInput
type="number"
:step="0.01"
v-model="advanceInvoiceData.part"
@focusout="advanceInvoiceData.partPerPecentage = Number((advanceInvoiceData.part / advanceInvoiceData.totalSumNet * 100).toFixed(2))"
/>
</UFormField>
</UCard>
</template>
</USlideover>
</UFormField>
<UFormField
label="Steuertyp:"
v-if="['invoices','advanceInvoices','confirmationOrders','serialInvoices', ...quoteLikeDocumentTypes].includes(itemInfo.type)"
>
<USelectMenu
:items="taxTypeItems"
v-model="itemInfo.taxType"
value-key="key"
label-key="label"
@update:model-value="setTaxType"
class="w-full"
></USelectMenu>
</UFormField>
<UFormField
label="Briefpapier:"
>
<USelectMenu
:items="letterheads"
v-model="itemInfo.letterhead"
value-key="id"
label-key="name"
:search-input="{ placeholder: 'Suche...' }"
:filter-fields="['name']"
class="w-full"
:color="itemInfo.letterhead ? 'primary' : 'error'"
>
<template #default>
{{ itemInfo.letterhead ? getLetterheadName(itemInfo.letterhead) : "Kein Briefpapier gewählt" }}
</template>
</USelectMenu>
</UFormField>
<UFormField
label="Kunde:"
>
<div class="flex flex-row">
<USelectMenu
:items="customers"
label-key="name"
value-key="id"
:filter-fields="['name']"
:search-input="{ placeholder: 'Suche...' }"
v-model="itemInfo.customer"
@update:model-value="setCustomerData"
:color="itemInfo.customer ? 'primary' : 'error'"
class="w-full min-w-0"
>
<template #item="{ item: option }">
{{ option.name }}{{ option.nameAddition }}
</template>
<template #default>
<span class="truncate">
{{ selectedCustomer ? selectedCustomer.name : "Kein Kunde ausgewählt" }}
</span>
</template>
</USelectMenu>
<EntityModalButtons
type="customers"
:id="itemInfo.customer"
@return-data="(data) => setCustomerData(data.id)"
/>
<!-- <UButton
variant="outline"
class="w-25 ml-2"
v-if="itemInfo.customer"
icon="i-heroicons-eye"
@click="modal.open(StandardEntityModal, {
id: itemInfo.customer,
type: 'customers',
mode: 'show',
})"
/>
<UButton
variant="outline"
class="w-25 ml-2"
v-if="itemInfo.customer"
icon="i-heroicons-pencil-solid"
@click="modal.open(StandardEntityModal, {
id: itemInfo.customer,
type: 'customers',
mode: 'edit',
onReturnData(data) {
setCustomerData()
}
})"
/>
<UButton
variant="outline"
class="w-25 ml-2"
v-if="!itemInfo.customer"
icon="i-heroicons-plus"
@click="modal.open(StandardEntityModal, {
type: 'customers',
mode: 'create',
onReturnData(data) {
setCustomerData(data.id)
}
})"
/>-->
</div>
<UAlert
v-if="selectedCustomer"
class="mt-2"
variant="soft"
color="neutral"
title="Info"
>
<template #description>
Kundennr: {{ formatNumberLikeValue(selectedCustomer.customerNumber) }}<br>
Typ: {{ selectedCustomer.isCompany ? 'Firma' : 'Privat' }}<br>
Notizen: <span class="truncate">{{ selectedCustomer.notes || '-' }}</span>
</template>
</UAlert>
</UFormField>
<UFormField
label="Ansprechpartner:"
v-if="selectedCustomer?.isCompany"
>
<InputGroup>
<USelectMenu
:items="contacts.filter(i => i.customer?.id === itemInfo.customer)"
label-key="fullName"
value-key="id"
:filter-fields="['fullName', 'name']"
:search-input="{ placeholder: 'Suche...' }"
v-model="itemInfo.contact"
:color="itemInfo.contact ? 'primary' : 'error'"
:disabled="!itemInfo.customer"
class="w-full min-w-0"
>
<template #default>
<span
class="truncate">{{ itemInfo.contact ? getContactName(itemInfo.contact) : "Kein Kontakt ausgewählt" }}</span>
</template>
</USelectMenu>
<!-- <UButton
variant="outline"
v-if="itemInfo.contact"
icon="i-heroicons-arrow-right-end-on-rectangle"
@click="router.push(`/standardEntity/contacts/show/${itemInfo.contact}`)"
>Kontakt</UButton>-->
<UButton
variant="outline"
color="error"
v-if="itemInfo.contact"
icon="i-heroicons-x-mark"
@click="itemInfo.contact = null"
/>
<EntityModalButtons
type="contacts"
:id="itemInfo.contact"
:create-query="{customer: itemInfo.customer}"
@return-data="(data) => itemInfo.contact = data.id"
/>
</InputGroup>
</UFormField>
<UFormField
label="Adresse:"
>
<UInput
v-model="itemInfo.address.street"
:placeholder="selectedCustomer?.infoData?.street || 'Straße + Hausnummer'"
:color="itemInfo.address.street ? 'primary' : 'error'"
class="w-full"
/>
<UInput
v-model="itemInfo.address.special"
class="mt-3 w-full"
:placeholder="selectedCustomer?.infoData?.special || 'Adresszusatz'"
/>
<InputGroup class="mt-3 w-full">
<UInput
class="w-full min-w-0"
v-model="itemInfo.address.zip"
type="text"
inputmode="numeric"
maxlength="5"
@input="sanitizeAddressZipInput"
@change="checkAddressZip"
:placeholder="selectedCustomer?.infoData?.zip || 'PLZ'"
:color="itemInfo.address.zip ? 'primary' : 'error'"
/>
<UInput
class="w-full min-w-0"
v-model="itemInfo.address.city"
:placeholder="selectedCustomer?.infoData?.city || 'Ort'"
:color="itemInfo.address.city ? 'primary' : 'error'"
/>
</InputGroup>
</UFormField>
</div>
<div class="w-2/3 min-w-0">
<InputGroup class="w-full items-start flex-wrap md:flex-nowrap">
<UFormField
:label="itemInfo.documentNumberTitle + ':'"
v-if="itemInfo.type !== 'serialInvoices'"
class="min-w-0 flex-1 mr-1"
>
<UInput
v-model="itemInfo.documentNumber"
placeholder="XXXX"
disabled
class="w-full"
/>
</UFormField>
<UFormField
class="min-w-0 flex-1 mr-1"
label="Lieferdatumsart:"
v-if="itemInfo.type !== 'serialInvoices'"
>
<USelectMenu
:items="deliveryDateTypeItems"
v-model="itemInfo.deliveryDateType"
class="w-full"
/>
</UFormField>
<UFormField
:label="`${itemInfo.deliveryDateType}${['Lieferzeitraum', 'Leistungszeitraum'].includes(itemInfo.deliveryDateType) ? ' Start' : ''}:`"
v-if="itemInfo.type !== 'serialInvoices' && itemInfo.deliveryDateType !== 'Kein Lieferdatum anzeigen'"
class="min-w-0 flex-1 mr-1"
>
<UPopover :content="{ side: 'bottom', align: 'start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="getDateButtonLabel(itemInfo.deliveryDate)"
variant="outline"
class="w-full justify-start"
/>
<template #content>
<div class="p-2">
<UCalendar
:model-value="getCalendarValue(itemInfo.deliveryDate)"
@update:model-value="(value) => setCalendarField('deliveryDate', value)"
:week-starts-on="1"
/>
<div class="flex justify-end px-2 pb-2">
<UButton
size="xs"
color="gray"
variant="soft"
icon="i-heroicons-calendar-days"
label="Heute"
@click="setCalendarFieldToToday('deliveryDate')"
/>
</div>
</div>
</template>
</UPopover>
</UFormField>
<UFormField
:label="itemInfo.deliveryDateType + ' Ende:'"
v-if="itemInfo.type !== 'serialInvoices' && ['Lieferzeitraum','Leistungszeitraum'].includes(itemInfo.deliveryDateType)"
class="min-w-0 flex-1"
>
<UPopover
:content="{ side: 'bottom', align: 'start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="getDateButtonLabel(itemInfo.deliveryDateEnd)"
variant="outline"
class="w-full justify-start"
/>
<template #content>
<div class="p-2">
<UCalendar
:model-value="getCalendarValue(itemInfo.deliveryDateEnd)"
@update:model-value="(value) => setCalendarField('deliveryDateEnd', value)"
:week-starts-on="1"
/>
<div class="flex justify-end px-2 pb-2">
<UButton
size="xs"
color="gray"
variant="soft"
icon="i-heroicons-calendar-days"
label="Heute"
@click="setCalendarFieldToToday('deliveryDateEnd')"
/>
</div>
</div>
</template>
</UPopover>
</UFormField>
</InputGroup>
<InputGroup class="w-full items-start flex-wrap md:flex-nowrap">
<UFormField
label="Belegdatum:"
class="min-w-0 flex-1 mr-1"
v-if="itemInfo.type !== 'serialInvoices'"
>
<UPopover :content="{ side: 'bottom', align: 'start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="getDateButtonLabel(itemInfo.documentDate)"
variant="outline"
class="w-full justify-start"
/>
<template #content>
<div class="p-2">
<UCalendar
:model-value="getCalendarValue(itemInfo.documentDate)"
@update:model-value="(value) => setCalendarField('documentDate', value)"
:week-starts-on="1"
/>
<div class="flex justify-end px-2 pb-2">
<UButton
size="xs"
color="gray"
variant="soft"
icon="i-heroicons-calendar-days"
label="Heute"
@click="setCalendarFieldToToday('documentDate')"
/>
</div>
</div>
</template>
</UPopover>
</UFormField>
<UFormField
class="min-w-0 flex-1"
label="Zahlungsziel in Tagen:"
>
<UInput
type="number"
v-model="itemInfo.paymentDays"
class="w-full"
/>
</UFormField>
<UFormField
class="min-w-0 flex-1"
label="Individueller Aufschlag:"
>
<UInput
type="number"
step="0.01"
v-model="itemInfo.customSurchargePercentage"
@change="updateCustomSurcharge"
class="w-full"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">%</span>
</template>
</UInput>
</UFormField>
</InputGroup>
<InputGroup class="w-full items-start flex-wrap md:flex-nowrap">
<UFormField
class="min-w-0 flex-1"
label="Zahlungsart:"
>
<USelectMenu
v-model="itemInfo.payment_type"
value-key="key"
label-key="label"
:items="paymentTypeItems"
class="w-full"
>
<template #default>
{{itemInfo.payment_type === 'transfer' ? "Überweisung" : "SEPA-Lastschrift"}}
</template>
</USelectMenu>
</UFormField>
<UFormField
v-if="itemInfo.payment_type === 'direct-debit'"
class="min-w-0 flex-1"
label="SEPA-Mandat:"
>
<InputGroup>
<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>
{{ getSelectedSepaMandate()?.reference || "Kein Mandat ausgewählt" }}
</template>
<template #item="{ item: mandate }">
{{ mandate.reference }} - {{ mandate.bankaccount?.displayLabel || "Bankverbindung" }}
</template>
</USelectMenu>
<UButton
variant="outline"
color="error"
v-if="itemInfo.outgoingsepamandate"
icon="i-heroicons-x-mark"
@click="itemInfo.outgoingsepamandate = null"
/>
<EntityModalButtons
type="outgoingsepamandates"
:id="itemInfo.outgoingsepamandate"
:create-query="{customer: itemInfo.customer}"
@return-data="(data) => itemInfo.outgoingsepamandate = data.id"
/>
</InputGroup>
</UFormField>
</InputGroup>
<UFormField
label="Mitarbeiter:"
>
<USelectMenu
:items="tenantUsers"
v-model="itemInfo.created_by"
label-key="full_name"
value-key="id"
@update:model-value="setContactPersonData"
class="w-full"
/>
</UFormField>
<UFormField
label="Kontakt Telefon:"
>
<UInput
v-model="itemInfo.contactTel"
class="w-full"
/>
</UFormField>
<UFormField
label="Kontakt E-Mail:"
>
<UInput
v-model="itemInfo.contactEMail"
class="w-full"
/>
</UFormField>
<UFormField
v-if="['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(itemInfo.type)"
label="Im Kundenportal anzeigen:"
>
<USwitch v-model="itemInfo.availableInPortal" />
</UFormField>
<UFormField
label="Objekt:"
>
<InputGroup>
<USelectMenu
:items="plants.filter(i => i.customer?.id === itemInfo.customer)"
v-model="itemInfo.plant"
value-key="id"
label-key="name"
:search-input="{ placeholder: 'Suche...' }"
:filter-fields="['name']"
class="w-full"
:disabled="!itemInfo.customer"
>
<template #default>
{{ itemInfo.plant ? getPlantName(itemInfo.plant) : "Kein Objekt ausgewählt" }}
</template>
<template #item="{ item: plant }">
{{ plant.name }}
</template>
</USelectMenu>
<UButton
variant="outline"
color="error"
v-if="itemInfo.plant"
icon="i-heroicons-x-mark"
@click="itemInfo.plant = null"
/>
<EntityModalButtons
type="plants"
:id="itemInfo.plant"
:create-query="{customer: itemInfo.customer}"
@return-data="(data) => itemInfo.plant = data.id"
/>
</InputGroup>
</UFormField>
<UFormField
label="Projekt:"
>
<InputGroup>
<USelectMenu
:items="projects.filter(i => i.customer?.id === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))"
v-model="itemInfo.project"
value-key="id"
label-key="name"
:search-input="{ placeholder: 'Suche...' }"
:filter-fields="['name']"
class="w-full"
:disabled="!itemInfo.customer"
>
<template #default>
{{ itemInfo.project ? getProjectName(itemInfo.project) : "Kein Projekt ausgewählt" }}
</template>
<template #item="{ item: project }">
{{ customers.find(i => i.id === project.customer) ? customers.find(i => i.id === project.customer).name : "" }}
- {{ project.name }}
</template>
</USelectMenu>
<UButton
variant="outline"
color="error"
v-if="itemInfo.project"
icon="i-heroicons-x-mark"
@click="itemInfo.project = null"
/>
<EntityModalButtons
type="projects"
:id="itemInfo.project"
:create-query="{customer: itemInfo.customer, plant: itemInfo.plant}"
@return-data="(data) => itemInfo.project = data.id"
/>
</InputGroup>
</UFormField>
<UFormField
label="Kostenstelle:"
>
<InputGroup>
<USelectMenu
:items="costcentres"
v-model="itemInfo.costcentre"
value-key="id"
label-key="name"
:search-input="{ placeholder: 'Suche...' }"
:filter-fields="['name', 'number']"
class="w-full"
>
<template #default>
{{ getCostCentreName(itemInfo.costcentre) }}
</template>
<template #item="{ item: costcentre }">
{{ [costcentre.number, costcentre.name].filter(Boolean).join(" - ") }}
</template>
</USelectMenu>
<UButton
variant="outline"
color="error"
v-if="itemInfo.costcentre"
icon="i-heroicons-x-mark"
@click="itemInfo.costcentre = null"
/>
<EntityModalButtons
type="costcentres"
:id="itemInfo.costcentre"
@return-data="(data) => itemInfo.costcentre = data.id"
/>
</InputGroup>
</UFormField>
<UFormField
label="Vertrag:"
>
<InputGroup>
<USelectMenu
:items="contracts.filter(i => (i.customer?.id || i.customer) === itemInfo.customer && (itemInfo.plant ? itemInfo.plant === i.plant : true))"
v-model="itemInfo.contract"
value-key="id"
label-key="name"
:search-input="{ placeholder: 'Suche...' }"
:filter-fields="['contractNumber','name']"
class="w-full"
:disabled="!itemInfo.customer"
>
<template #default>
{{ itemInfo.contract ? getContractName(itemInfo.contract) : "Kein Vertrag ausgewählt" }}
</template>
<template #item="{ item: contract }">
{{ customers.find(i => i.id === contract.customer) ? customers.find(i => i.id === contract.customer).name : "" }}
- {{ contract.contractNumber }} - {{ contract.name }}
</template>
</USelectMenu>
<UButton
variant="outline"
color="error"
v-if="itemInfo.contract"
icon="i-heroicons-x-mark"
@click="itemInfo.contract = null"
/>
<EntityModalButtons
type="contracts"
:id="itemInfo.contract"
:create-query="{customer: itemInfo.customer}"
@return-data="(data) => itemInfo.contract = data.id"
/>
</InputGroup>
</UFormField>
</div>
</InputGroup>
<div v-if="itemInfo.type === 'serialInvoices'" class="mb-5">
<USeparator class="mt-5 mb-3">
Einstellungen für die Serienrechnung
</USeparator>
<div class="flex flex-row w-full items-start flex-wrap md:flex-nowrap">
<div class="w-1/3 min-w-0">
<UFormField
label="Datum erste Ausführung:"
>
<UPopover :content="{ side: 'bottom', align: 'start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="getDateButtonLabel(itemInfo.serialConfig.firstExecution)"
variant="outline"
class="w-full justify-start"
/>
<template #content>
<div class="p-2">
<UCalendar
:model-value="getCalendarValue(itemInfo.serialConfig.firstExecution)"
@update:model-value="(value) => setSerialCalendarField('firstExecution', value)"
:week-starts-on="1"
/>
<div class="flex justify-end px-2 pb-2">
<UButton
size="xs"
color="gray"
variant="soft"
icon="i-heroicons-calendar-days"
label="Heute"
@click="setSerialCalendarFieldToToday('firstExecution')"
/>
</div>
</div>
</template>
</UPopover>
</UFormField>
<UFormField
label="Datum letzte Ausführung:"
>
<UPopover :content="{ side: 'bottom', align: 'start' }">
<UButton
icon="i-heroicons-calendar-days-20-solid"
:label="getDateButtonLabel(itemInfo.serialConfig.executionUntil)"
variant="outline"
class="w-full justify-start"
/>
<template #content>
<div class="p-2">
<UCalendar
:model-value="getCalendarValue(itemInfo.serialConfig.executionUntil)"
@update:model-value="(value) => setSerialCalendarField('executionUntil', value)"
:week-starts-on="1"
/>
<div class="flex justify-end px-2 pb-2">
<UButton
size="xs"
color="gray"
variant="soft"
icon="i-heroicons-calendar-days"
label="Heute"
@click="setSerialCalendarFieldToToday('executionUntil')"
/>
</div>
</div>
</template>
</UPopover>
</UFormField>
<UCheckbox
v-model="itemInfo.serialConfig.active"
label="Aktiv"
class="mt-3"
/>
</div>
<div class="w-2/3 min-w-0">
<UFormField
label="Intervall:"
>
<USelectMenu
v-model="itemInfo.serialConfig.intervall"
:items="serialIntervalItems"
class="w-full"
/>
</UFormField>
<UFormField
label="Richtung:"
>
<USelectMenu
v-model="itemInfo.serialConfig.dateDirection"
:items="serialDateDirectionItems"
class="w-full"
/>
</UFormField>
<UAlert
title="Anfangs- und Enddatum"
description="Für das Anfangs- und Enddatum werden jeweils der ersten und letzte Tag des ausgewählten Intervalls und der Richtung automatisch ausgewählt"
class="mt-5"
/>
</div>
</div>
</div>
<USeparator
class="my-3"
/>
<UFormField
label="Titel:"
>
<UInput v-model="itemInfo.title" disabled class="w-full"/>
</UFormField>
<UFormField
label="Beschreibung:"
class="mt-3"
>
<UInput v-model="itemInfo.description" class="w-full"/>
</UFormField>
<USeparator
class="my-3"
/>
<UFormField
label="Vorlage auswählen"
>
<USelectMenu
:items="getTextTemplateByType(itemInfo.type,'startText')"
v-model="itemInfo.startText"
label-key="text"
value-key="text"
class="w-full"
>
<template #item="{ item: option }">
{{ option.name }} - {{ option.text }}
</template>
<template #default>
{{ texttemplates.find(i => i.text === itemInfo.startText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)) ? texttemplates.find(i => i.text === itemInfo.startText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)).name : "Keine Vorlage ausgewählt oder Vorlage verändert" }}
</template>
</USelectMenu>
</UFormField>
<UFormField
label="Einleitung:"
>
<UTextarea
v-model="itemInfo.startText"
:rows="6"
class="w-full"
/>
</UFormField>
<USeparator
class="my-3"
/>
<table class="w-full" v-if="itemInfo.rows.length > 0">
<thead>
<tr>
<th></th>
<th>Pos.</th>
<th class="pl-2">Name</th>
<th class="pl-2">Menge</th>
<th class="pl-2">Einheit</th>
<th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">Preis</th>
<th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">EK</th>
<!-- <th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">Steuer</th>
<th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">Rabatt</th>-->
<th class="pl-2"></th>
<th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">Gesamt</th>
<th class="pl-2"></th>
<th class="pl-2"></th>
</tr>
</thead>
<draggable
v-model="itemInfo.rows"
handle=".handle"
tag="tbody"
itemKey="pos"
@end="setPosNumbers"
>
<template #item="{element: row}">
<tr :class="{ collapse: isDocumentRowHidden(row.id) }">
<td>
<div class="flex items-center gap-1 whitespace-nowrap">
<UIcon
class="handle shrink-0"
name="i-mdi-menu"
v-if="itemInfo.type !== 'cancellationInvoices'"
/>
<UTooltip
v-if="row.mode === 'title'"
:text="collapsedTitleRowIds.has(row.id) ? 'Titelbereich ausklappen' : 'Titelbereich einklappen'"
>
<UButton
class="shrink-0"
variant="ghost"
color="neutral"
:icon="collapsedTitleRowIds.has(row.id) ? 'i-heroicons-chevron-right' : 'i-heroicons-chevron-down'"
:aria-label="collapsedTitleRowIds.has(row.id) ? 'Titelbereich ausklappen' : 'Titelbereich einklappen'"
@click="toggleTitleSection(row.id)"
/>
</UTooltip>
</div>
</td>
<td
v-if="row.mode === 'pagebreak'"
:colspan="getFullWidthPositionColspan()"
>
<USeparator/>
</td>
<td
v-if="row.mode === 'text'"
:colspan="getFullWidthPositionColspan()"
>
<!-- <UInput
v-model="row.text"
placeholder="Titel"
maxlength="60"
/>-->
<UTextarea
v-model="row.description"
placeholder="Text"
class="w-full"
:rows="4"
autoresize
/>
</td>
<td
v-if="!['pagebreak','text'].includes(row.mode)"
>{{ row.pos }}
</td>
<td
class="w-full"
v-if="row.mode === 'free'"
>
<UInput
:disabled="itemInfo.type === 'cancellationInvoices' || row.editDisabled"
v-model="row.text"
placeholder="Name"
class="w-full min-w-0"
/>
</td>
<td
class="w-full"
v-else-if="row.mode === 'normal'"
>
<InputGroup class="w-full">
<div class="w-full min-w-0">
<USelectMenu
:disabled="itemInfo.type === 'cancellationInvoices'"
class="w-full min-w-0"
:items="products"
:color="row.product ? 'primary' : 'error'"
label-key="name"
value-key="id"
:search-input="{ placeholder: 'Suche ...' }"
:filter-fields="['name']"
v-model="row.product"
@update:model-value="() => setRowData(row)"
>
<template #default>
<span class="truncate">{{
getSelectedProduct(row) ? getSelectedProduct(row).name : "Kein Produkt ausgewählt"
}}</span>
</template>
</USelectMenu>
<div
v-if="normalizeExternalUrl(getSelectedProduct(row)?.supplier_link)"
class="mt-1 text-xs"
>
<a
:href="normalizeExternalUrl(getSelectedProduct(row)?.supplier_link)"
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:underline break-all"
>
Lieferantenlink öffnen
</a>
</div>
</div>
<EntityModalButtons
type="products"
:id="row.product"
@return-data="(data) => setRowData(row,null,data)"
/>
<!-- <UButton
icon="i-heroicons-magnifying-glass"
@click="showProductSelectionModal = true"
/>
<UModal v-model:open="showProductSelectionModal">
<UCard>
<template #header>
Artikel Auswählen
</template>
<InputGroup class="w-full">
<UFormField label="Artikelkategorie:">
<USelectMenu
v-if="productcategories.length > 0"
:options="[{name: 'Nicht zugeordnet',id:'not set'},...productcategories]"
value-attribute="id"
option-attribute="name"
v-model="selectedProductcategorie"
/>
</UFormField>
</InputGroup>
<UTable
:rows="selectedProductcategorie !== 'not set' ? products.filter(i => i.productcategories.includes(selectedProductcategorie)) : products.filter(i => i.productcategories.length === 0)"
:columns="normalizeTableColumns([
{key: 'name',label:'Name'},
{key: 'manufacturer',label:'Hersteller'},
{key: 'articleNumber',label:'Artikelnummer'},
])"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Artikel anzuzeigen' }"
@select=" (i) => {
row.product = i.id
row.unit = products.find(i => i.id === row.product).unit,
row.price = (products.find(i => i.id === row.product).sellingPrice || 0),
row.description = products.find(i => i.id === row.product).description
showProductSelectionModal = false}"
>
</UTable>
</UCard>
</UModal>-->
</InputGroup>
</td>
<td
class="w-full"
v-else-if="row.mode === 'service'"
>
<InputGroup class="w-full">
<USelectMenu
:disabled="itemInfo.type === 'cancellationInvoices'"
class="w-full min-w-0"
:items="services"
:color="row.service ? 'primary' : 'error'"
label-key="name"
value-key="id"
:search-input="{ placeholder: 'Suche ...' }"
:filter-fields="['name']"
v-model="row.service"
@update:model-value="() => setRowData(row)"
>
<template #default>
<span class="truncate">{{
services.find(i => i.id === row.service) ? services.find(i => i.id === row.service).name : "Keine Leistung ausgewählt"
}}</span>
</template>
</USelectMenu>
<EntityModalButtons
type="services"
:id="row.service"
@return-data="(data) => setRowData(row,data,null)"
/>
<!-- <UButton
icon="i-heroicons-magnifying-glass"
@click="showServiceSelectionModal = true"
/>
<UModal v-model:open="showServiceSelectionModal">
<UCard>
<template #header>
Leistung Auswählen
</template>
<InputGroup class="w-full">
<UFormField label="Leistungskategorie:">
<USelectMenu
v-if="servicecategories.length > 0"
:options="[{name: 'Nicht zugeordnet',id:'not set'},...servicecategories]"
value-attribute="id"
option-attribute="name"
v-model="selectedServicecategorie"
/>
</UFormField>
</InputGroup>
<UTable
:rows="selectedServicecategorie !== 'not set' ? services.filter(i => i.servicecategories.includes(selectedServicecategorie)) : services.filter(i => i.servicecategories.length === 0)"
:columns="normalizeTableColumns([
{key: 'name',label:'Name'},
{key: 'serviceNumber',label:'Leistungsnummer'},
{key: 'sellingPrice',label:'Verkaufspreis'},
])"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Leistungen anzuzeigen' }"
@select=" (i) => {
row.service = i.id
setRowData(row)
showServiceSelectionModal = false}"
>
</UTable>
</UCard>
</UModal>-->
</InputGroup>
</td>
<td
class="w-full"
v-if="!['pagebreak','title','text'].includes(row.mode)"
>
<UInput
v-model="row.quantity"
:disabled="itemInfo.type === 'cancellationInvoices'"
type="number"
:step="units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).step : '1' "
class="w-20"
/>
</td>
<td
class="w-full"
v-if="!['pagebreak','title','text'].includes(row.mode)"
>
<USelectMenu
v-model="row.unit"
:disabled="itemInfo.type === 'cancellationInvoices'"
:items="units"
label-key="name"
value-key="id"
class="w-full"
>
<template #default>
{{ units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).name : "Keine Einheit gewählt" }}
</template>
</USelectMenu>
</td>
<td
class="w-full"
v-if="!['pagebreak','title','text'].includes(row.mode) && !deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
>
<UInput
v-model="row.inputPrice"
:disabled="itemInfo.type === 'cancellationInvoices'"
type="number"
step="0.001"
@change="updateCustomSurcharge"
class="w-30"
>
<template #leading>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
<template #trailing>
<span
v-if="row.price > row.inputPrice"
class="text-primary text-xs">
{{ useCurrency(row.price) }}
</span>
<span
v-else-if="row.price < row.inputPrice"
class="text-rose-600 text-xs">
{{ useCurrency(row.price) }}
</span>
<span
v-else
class="text-gray-500 dark:text-gray-400 text-xs"> </span>
</template>
</UInput>
</td>
<td
class="w-full"
v-if="!['pagebreak','title','text'].includes(row.mode) && !deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
>
<UInput
v-if="row.mode === 'free'"
v-model="row.purchasePrice"
:disabled="itemInfo.type === 'cancellationInvoices'"
type="number"
step="0.001"
class="w-28"
>
<template #leading>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
</UInput>
<span v-else class="text-gray-400">-</span>
</td>
<!-- <td
class="w-40"
v-if="!['pagebreak','title','text'].includes(row.mode)&& !deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
>
<USelectMenu
:options="[19,7,0]"
v-model="row.taxPercent"
:disabled="itemInfo.taxType === '13b UStG' || itemInfo.taxType === '19 UStG'"
>
<template #option="{option}">
{{option}} %
</template>
<template #label>
{{row.taxPercent}} %
</template>
</USelectMenu>
</td>-->
<!-- <td
class="w-40"
v-if="!['pagebreak','title','text'].includes(row.mode)&& !deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
>
<UInput
v-model="row.discountPercent"
type="number"
step="0.01"
placeholder="0"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">%</span>
</template>
</UInput>
</td>-->
<td
class="w-40"
v-if="!['pagebreak','title','text'].includes(row.mode)"
>
<UButton
:disabled="itemInfo.type === 'cancellationInvoices'"
icon="i-heroicons-pencil-square-16-solid"
@click="row.showEdit = true"
/>
<UButton
:disabled="itemInfo.type === 'cancellationInvoices'"
icon="i-mdi-water-drop-outline"
class="ml-3"
v-if="row.agriculture"
@click="row.showEditDiesel = true"
/>
<UModal v-model:open="row.showEdit">
<template #content>
<UCard>
<!-- <template #header>
Zeile bearbeiten
</template>-->
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Zeile bearbeiten
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1"
@click="row.showEdit = false"/>
</div>
</template>
<InputGroup class="w-full items-start">
<UFormField
label="Anzahl:"
class="w-full min-w-0"
>
<UInput
v-model="row.quantity"
type="number"
:step="units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).step : '1' "
class="w-full"
/>
</UFormField>
<UFormField
label="Einheit:"
class="w-full min-w-0"
>
<USelectMenu
v-model="row.unit"
:items="units"
label-key="name"
value-key="id"
class="w-full"
>
<template #default>
{{ units.find(i => i.id === row.unit) ? units.find(i => i.id === row.unit).name : "Keine Einheit gewählt" }}
</template>
</USelectMenu>
</UFormField>
</InputGroup>
<UFormField
label="Kostenstelle:"
class="mt-3"
>
<USelectMenu
v-model="row.costCentre"
:items="costcentres"
label-key="name"
value-key="id"
:search-input="{ placeholder: 'Suche...' }"
:filter-fields="['name', 'number']"
class="w-full"
>
<template #default>
<span class="truncate">{{ getCostCentreName(row.costCentre) }}</span>
</template>
<template #item="{ item: costcentre }">
{{ [costcentre.number, costcentre.name].filter(Boolean).join(" - ") }}
</template>
</USelectMenu>
</UFormField>
<UFormField
label="Einzelpreis:"
v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
class="mt-3"
>
<UInput
v-model="row.inputPrice"
type="number"
step="0.001"
@change="updateCustomSurcharge"
class="w-full"
>
<template #leading>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
<template #trailing>
<span
v-if="row.price > row.inputPrice"
class="text-primary text-xs">
{{ useCurrency(row.price) }}
</span>
<span
v-else-if="row.price < row.inputPrice"
class="text-rose-600 text-xs">
{{ useCurrency(row.price) }}
</span>
<span
v-else
class="text-gray-500 dark:text-gray-400 text-xs"> </span>
</template>
</UInput>
</UFormField>
<UFormField
label="Umsatzsteuer:"
class="mt-3"
v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
>
<USelectMenu
:items="taxPercentItems"
v-model="row.taxPercent"
:disabled="itemInfo.taxType === '13b UStG' || itemInfo.taxType === '19 UStG'"
class="w-full"
>
<template #item="{ item: option }">
{{ option }} %
</template>
<template #default>
{{ row.taxPercent }} %
</template>
</USelectMenu>
</UFormField>
<UFormField
label="Rabatt:"
class="mt-3"
v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
>
<UInput
v-model="row.discountPercent"
type="number"
step="0.01"
placeholder="0"
class="w-full"
>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">%</span>
</template>
</UInput>
</UFormField>
<InputGroup class="w-full mt-3">
<UFormField
label="Optional:"
>
<USwitch
:disabled="row.alternative"
v-model="row.optional"
/>
</UFormField>
<UFormField
class="ml-3"
label="Alternativ:"
>
<USwitch
:disabled="row.optional"
v-model="row.alternative"
/>
</UFormField>
</InputGroup>
<UAlert
v-if="isImportedAdvanceInvoiceRow(row)"
title="Berechnung des Individuellen Aufschlags für die Zeile gesperrt"
color="orange"
variant="subtle"
class="my-3"
/>
Verknüpfungen:
<ul>
<li
v-for="linkedEntity in row.linkedEntitys"
>
- <a
v-if="linkedEntity.subtype === 'advanceInvoices'"
:to="`/createddocuments/show/${linkedEntity.id}`"
>Abschlagsrechnung</a>
</li>
</ul>
<UFormField
label="Beschreibung:"
class="mt-3"
>
<UTextarea
v-model="row.description"
rows="5"
class="w-full"
>
</UTextarea>
</UFormField>
<!-- <template #footer>
<UButton
@click="row.showEdit = false"
>
Speichern
</UButton>
</template>-->
</UCard>
</template>
</UModal>
<UModal v-model:open="row.showEditDiesel">
<template #content>
<UCard>
<template #header>
Dieselverbrauch bearbeiten
</template>
<UFormField
label="Menge Diesel:"
>
<UInput
v-model="row.agriculture.dieselUsage"
@change="row.agriculture.adblueUsage = row.agriculture.dieselUsage * 0.04"
@input="row.agriculture.adblueUsage = row.agriculture.dieselUsage * 0.04"
type="number"
steps="0.01"
class="w-full"
>
<template #trailing>
L
</template>
</UInput>
</UFormField>
<UFormField
label="Preis Diesel:"
>
<UInput
v-model="row.agriculture.dieselPrice"
@change="row.agriculture.adblueUsage = row.agriculture.dieselUsage * 0.04"
type="number"
steps="0.01"
class="w-full"
>
<template #trailing>
€/L
</template>
</UInput>
</UFormField>
<UFormField
label="Menge AdBlue:"
>
<UInput
v-model="row.agriculture.adblueUsage"
type="number"
steps="0.01"
class="w-full"
>
<template #trailing>
L
</template>
</UInput>
</UFormField>
<UFormField
label="Preis AdBlue:"
>
<UInput
v-model="row.agriculture.adbluePrice"
type="number"
steps="0.01"
class="w-full"
>
<template #trailing>
€/L
</template>
</UInput>
</UFormField>
<template #footer>
<UButton
@click="row.showEditDiesel = false,
processDieselPosition()"
>
Speichern
</UButton>
</template>
</UCard>
</template>
</UModal>
</td>
<td
v-if="!['pagebreak','title','text'].includes(row.mode) && !deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
>
<p class="text-right font-bold whitespace-nowrap">
{{ row.optional || row.alternative ? "(" : "" }}
<span v-if="row.discountPercent !== 0" class="line-through mr-2 text-rose-500">
{{ getRowAmountUndiscounted(row) }} €
</span>
{{ getRowAmount(row) }} €
{{ row.optional || row.alternative ? ")" : "" }}
</p>
</td>
<td
v-if="row.mode === 'title'"
:colspan="getTitlePositionColspan()"
>
<InputGroup class="w-full">
<USelect
:disabled="itemInfo.type === 'cancellationInvoices'"
v-model="row.titleLevel"
:items="titleLevelItems"
value-key="value"
class="w-44 shrink-0"
@update:model-value="setPosNumbers"
/>
<UInput
:disabled="itemInfo.type === 'cancellationInvoices'"
v-model="row.text"
placeholder="Titel"
class="w-full min-w-0"
/>
</InputGroup>
</td>
<td>
<UDropdownMenu
:items="getPositionAddMenuItems(row)"
:content="{ align: 'end' }"
>
<UButton
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
variant="ghost"
color="primary"
icon="i-heroicons-plus"
/>
</UDropdownMenu>
</td>
<td>
<UButton
:disabled="itemInfo.type === 'cancellationInvoices'"
variant="ghost"
color="error"
icon="i-heroicons-x-mark-16-solid"
@click="removePosition(row.id)"
/>
</td>
</tr>
</template>
</draggable>
</table>
<UAlert
v-else
title="Keine Positionen hinzugefügt"
color="error"
variant="outline"
icon="i-heroicons-light-bulb"
></UAlert>
<InputGroup>
<UButton
v-for="option in positionAddOptions"
:key="option.mode"
@click="addPosition(option.mode)"
class="mt-3"
:disabled="['advanceInvoices','cancellationInvoices'].includes(itemInfo.type)"
>
+ {{ option.label }}
</UButton>
</InputGroup>
<!-- <USeparator
class="mt-5 mb-3"
v-if="openAdvanceInvoices.length > 0 || itemInfo.usedAdvanceInvoices.length > 0"
>
Noch nicht abgerechnete Abschlagsrechnungen
</USeparator>
<div
v-for="advanceInvoice in openAdvanceInvoices"
:key="advanceInvoice.id"
class="flex flex-row justify-between my-2"
>
<span>{{advanceInvoice.documentNumber}}</span>
<span>Summe: {{renderCurrency(advanceInvoice.rows[0].price * (1 + advanceInvoice.rows[0].taxPercent/100))}}</span>
<InputGroup>
<UButton
@click="addAdvanceInvoiceToInvoice(advanceInvoice.id)"
:disabled="itemInfo.usedAdvanceInvoices.includes(advanceInvoice.id)"
class="mr-2"
>
Verwenden
</UButton>
<UButton
@click="itemInfo.usedAdvanceInvoices = itemInfo.usedAdvanceInvoices.filter(i => i !== advanceInvoice.id)"
:disabled="!itemInfo.usedAdvanceInvoices.includes(advanceInvoice.id)"
color="error"
variant="outline"
>
X
</UButton>
</InputGroup>
</div>-->
<USeparator class="my-3" v-if="Object.keys(documentTotal.titleSums).length > 0" label="Überschriften"/>
<table>
<tr v-for="sumKey in Object.keys(documentTotal.titleSums) ">
<td>{{ sumKey }}</td>
<td>{{ useCurrency(documentTotal.titleSums[sumKey]) }}</td>
</tr>
</table>
<USeparator class="my-3" v-if="itemInfo.rows.length > 0" label="Auswertung & Gesamt"/>
<div class="w-full flex justify-between" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">
<table class="w-1/2" v-if="itemInfo.rows.length > 0">
<tr v-if="documentReport.totalProductsPurchasePrice !== 0">
<td>Einkaufspreis Artikel Gesamt:</td>
<td class="text-right">{{ useCurrency(documentReport.totalProductsPurchasePrice) }}</td>
</tr>
<tr v-if="documentReport.totalProductsFromServicesPurchasePrice !== 0">
<td>Einkaufspreis Artikel aus Leistungen Gesamt:</td>
<td class="text-right">{{ useCurrency(documentReport.totalProductsFromServicesPurchasePrice) }}</td>
</tr>
<tr v-if="documentReport.totalHoursFromServices.totalPurchasePrice !== 0">
<td>Einkaufspreis Personal aus Leistungen Gesamt:</td>
<td class="text-right">{{ useCurrency(documentReport.totalHoursFromServices.totalPurchasePrice) }}</td>
</tr>
<tr>
<td>Gewinn Gesamt:</td>
<td class="text-right">{{ useCurrency(documentReport.totalMargin) }}</td>
</tr>
<tr v-if="documentReport.totalHoursSellingPrice !== 0">
<td>Lohnkosten Verkauf:</td>
<td class="text-right">{{ useCurrency(documentReport.totalHoursSellingPrice) }}</td>
</tr>
<tr v-for="key in Object.keys(documentReport.totalHoursFromServices.byName)">
<td>{{ key }}</td>
<td class="text-right">{{ documentReport.totalHoursFromServices.byName[key] }} h</td>
</tr>
<tr v-if="documentReport.totalHoursFromServices.total !== 0">
<td>Stunden Gesamt:</td>
<td class="text-right">{{ documentReport.totalHoursFromServices.total }} h</td>
</tr>
</table>
<table class="w-1/3 h-full" v-if="itemInfo.rows.length > 0">
<tr>
<td class="font-bold">Netto:</td>
<td class="text-right">{{ renderCurrency(documentTotal.totalNet) }}</td>
</tr>
<tr v-if="itemInfo.taxType === 'Standard' && itemInfo.rows.find(i => i.taxPercent === 19)">
<td class="font-bold">zzgl. 19 % USt auf {{ renderCurrency(documentTotal.totalNet19) }}:</td>
<td class="text-right">{{ renderCurrency(documentTotal.total19) }}</td>
</tr>
<tr v-if="itemInfo.taxType === 'Standard' && itemInfo.rows.find(i => i.taxPercent === 7)">
<td class="font-bold">zzgl. 7 % USt auf {{ renderCurrency(documentTotal.totalNet7) }}:</td>
<td class="text-right">{{ renderCurrency(documentTotal.total7) }}</td>
</tr>
<tr v-if="itemInfo.taxType === 'Standard' && itemInfo.rows.find(i => i.taxPercent === 0)">
<td class="font-bold">zzgl. 0 % USt auf {{ renderCurrency(documentTotal.totalNet0) }}:</td>
<td class="text-right">{{ renderCurrency(documentTotal.total0) }}</td>
</tr>
<tr>
<td class="font-bold">Brutto:</td>
<td class="text-right">{{ renderCurrency(documentTotal.totalGross) }}</td>
</tr>
<tr v-if="documentTotal.totalGrossAlreadyPaid !== 0">
<td class="font-bold">Bereits bezahlt:</td>
<td class="text-right">{{ renderCurrency(documentTotal.totalGrossAlreadyPaid) }}</td>
</tr>
<tr v-if="documentTotal.totalGrossAlreadyPaid !== 0">
<td class="font-bold">Offene Summe:</td>
<td class="text-right">{{ renderCurrency(documentTotal.totalSumToPay) }}</td>
</tr>
</table>
</div>
<!-- <USeparator
class="my-3"
>Auswertung</USeparator>
<div class="w-full flex justify-end">
<table class="w-1/3">
<tr v-if="documentReport.totalProductsPurchasePrice !== 0">
<td>Einkaufspreis Artikel Gesamt:</td>
<td class="text-right">{{useCurrency(documentReport.totalProductsPurchasePrice)}}</td>
</tr>
<tr v-if="documentReport.totalProductsFromServicesPurchasePrice !== 0">
<td>Einkaufspreis Artikel aus Leistungen Gesamt:</td>
<td class="text-right">{{useCurrency(documentReport.totalProductsFromServicesPurchasePrice)}}</td>
</tr>
<tr v-if="documentReport.totalHoursFromServices.totalPurchasePrice !== 0">
<td>Einkaufspreis Personal aus Leistungen Gesamt:</td>
<td class="text-right">{{useCurrency(documentReport.totalHoursFromServices.totalPurchasePrice)}}</td>
</tr>
<tr>
<td>Gewinn Gesamt:</td>
<td class="text-right">{{useCurrency(documentReport.totalMargin)}}</td>
</tr>
<tr v-if="documentReport.totalHoursSellingPrice !== 0">
<td>Lohnkosten Verkauf:</td>
<td class="text-right">{{useCurrency(documentReport.totalHoursSellingPrice)}}</td>
</tr>
<tr v-for="key in Object.keys(documentReport.totalHoursFromServices.byName)">
<td>{{key}}</td>
<td class="text-right">{{documentReport.totalHoursFromServices.byName[key]}} h</td>
</tr>
<tr v-if="documentReport.totalHoursFromServices.total !== 0">
<td>Stunden Gesamt:</td>
<td class="text-right">{{documentReport.totalHoursFromServices.total}} h</td>
</tr>
</table>
</div>-->
<USeparator
class="my-3"
/>
<UFormField
label="Vorlage auswählen"
>
<USelectMenu
:items="getTextTemplateByType(itemInfo.type,'endText')"
v-model="itemInfo.endText"
label-key="text"
value-key="text"
class="w-full"
>
<template #item="{ item: option }">
{{option.name}} - {{option.text}}
</template>
<template #default>
{{texttemplates.find(i => i.text === itemInfo.endText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)) ? texttemplates.find(i => i.text === itemInfo.endText && (itemInfo.type === "serialInvoices" ? i.documentType === "invoices" : i.documentType === itemInfo.type)).name : "Keine Vorlage ausgewählt oder Vorlage verändert"}}
</template>
</USelectMenu>
</UFormField>
<UFormField
label="Nachbemerkung:"
>
<UTextarea
v-model="itemInfo.endText"
:rows="6"
class="w-full"
/>
</UFormField>
</div>
<div v-else-if="item.label === 'Vorschau'">
<PDFViewer
v-if="showDocument"
:uri="uri"
location="edit_create_document"
/>
</div>
</template>
</UTabs>
<UProgress animation="carousel" v-else/>
</UDashboardPanelContent>
<PageLeaveGuard :when="guard"/>
</template>
<style scoped>
th {
text-align: left;
}
td {
padding: .4em;
}
/*tr:hover {
border: 1px solid #69c350;
}*/
.previewDocumentMobile {
aspect-ratio: 1 / 1.414;
}
.previewDocument {
height: 80vh;
}
</style>