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>
4049 lines
160 KiB
Vue
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>
|