Merge remote-tracking branch 'origin/dev' into dev
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 59s
Build and Push Docker Images / build-frontend (push) Successful in 35s
Build and Push Docker Images / build-website (push) Successful in 34s
Build and Push Docker Images / build-central-services-admin (push) Successful in 34s
Build and Push Docker Images / build-central-services-api (push) Successful in 35s
Build and Push Docker Images / build-docs (push) Successful in 34s
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 59s
Build and Push Docker Images / build-frontend (push) Successful in 35s
Build and Push Docker Images / build-website (push) Successful in 34s
Build and Push Docker Images / build-central-services-admin (push) Successful in 34s
Build and Push Docker Images / build-central-services-api (push) Successful in 35s
Build and Push Docker Images / build-docs (push) Successful in 34s
This commit is contained in:
40
backend/db/migrations/0061_additional_skr03_accounts.sql
Normal file
40
backend/db/migrations/0061_additional_skr03_accounts.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
WITH requested_accounts ("number", "label") AS (
|
||||
VALUES
|
||||
('1576', 'Abziehbare Vorsteuer 19 %'),
|
||||
('4110', 'Löhne'),
|
||||
('4115', 'Löhne (Konto 4115)'),
|
||||
('4120', 'Gehälter'),
|
||||
('4130', 'Gesetzliche soziale Aufwendungen'),
|
||||
('4140', 'Freiwillige soziale Aufwendungen (lohnsteuerfrei)')
|
||||
)
|
||||
UPDATE "accounts" AS account
|
||||
SET "label" = requested."label",
|
||||
"description" = NULL
|
||||
FROM requested_accounts AS requested
|
||||
WHERE account."accountChart" = 'skr03'
|
||||
AND account."number" = requested."number";
|
||||
--> statement-breakpoint
|
||||
WITH requested_accounts ("number", "label") AS (
|
||||
VALUES
|
||||
('1576', 'Abziehbare Vorsteuer 19 %'),
|
||||
('4110', 'Löhne'),
|
||||
('4115', 'Löhne (Konto 4115)'),
|
||||
('4120', 'Gehälter'),
|
||||
('4130', 'Gesetzliche soziale Aufwendungen'),
|
||||
('4140', 'Freiwillige soziale Aufwendungen (lohnsteuerfrei)')
|
||||
)
|
||||
INSERT INTO "accounts" ("number", "label", "description", "accountChart")
|
||||
SELECT requested."number", requested."label", NULL, 'skr03'
|
||||
FROM requested_accounts AS requested
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "accounts" AS account
|
||||
WHERE account."accountChart" = 'skr03'
|
||||
AND account."number" = requested."number"
|
||||
);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('accounts', 'id'),
|
||||
GREATEST(COALESCE((SELECT MAX("id") FROM "accounts"), 1), 1),
|
||||
true
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "incominginvoices" ADD COLUMN "tax_amount_override" numeric(12, 2);
|
||||
20
backend/db/migrations/0063_skr03_output_tax_account.sql
Normal file
20
backend/db/migrations/0063_skr03_output_tax_account.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
UPDATE "accounts"
|
||||
SET "label" = 'Umsatzsteuer 19 %',
|
||||
"description" = NULL
|
||||
WHERE "accountChart" = 'skr03'
|
||||
AND "number" = '1776';
|
||||
--> statement-breakpoint
|
||||
INSERT INTO "accounts" ("number", "label", "description", "accountChart")
|
||||
SELECT '1776', 'Umsatzsteuer 19 %', NULL, 'skr03'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "accounts"
|
||||
WHERE "accountChart" = 'skr03'
|
||||
AND "number" = '1776'
|
||||
);
|
||||
--> statement-breakpoint
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('accounts', 'id'),
|
||||
GREATEST(COALESCE((SELECT MAX("id") FROM "accounts"), 1), 1),
|
||||
true
|
||||
);
|
||||
@@ -407,6 +407,27 @@
|
||||
"when": 1786086000000,
|
||||
"tag": "0060_document_import_sources",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 58,
|
||||
"version": "7",
|
||||
"when": 1786280400000,
|
||||
"tag": "0061_additional_skr03_accounts",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 59,
|
||||
"version": "7",
|
||||
"when": 1786284000000,
|
||||
"tag": "0062_incoming_invoice_tax_override",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 60,
|
||||
"version": "7",
|
||||
"when": 1786287600000,
|
||||
"tag": "0063_skr03_output_tax_account",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
numeric,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
@@ -55,6 +56,8 @@ export const incominginvoices = pgTable("incominginvoices", {
|
||||
},
|
||||
]),
|
||||
|
||||
taxAmountOverride: numeric("tax_amount_override", { precision: 12, scale: 2 }),
|
||||
|
||||
paid: boolean("paid").notNull().default(false),
|
||||
expense: boolean("expense").notNull().default(true),
|
||||
|
||||
|
||||
@@ -41,6 +41,60 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
const ManualInvoices = aliasedTable(incominginvoices, "manual_invoices")
|
||||
const ManualInvoiceVendors = aliasedTable(vendors, "manual_invoice_vendors")
|
||||
|
||||
const useCurrentIncomingInvoiceAmount = async (tenantId: number, allocation: any) => {
|
||||
const invoiceId = Number(allocation.incominginvoice)
|
||||
const statementId = Number(allocation.bankstatement)
|
||||
if (!invoiceId || !statementId) return allocation
|
||||
|
||||
const [[invoice], [statement], invoiceAllocations, statementAllocations] = await Promise.all([
|
||||
server.db.select().from(incominginvoices).where(and(
|
||||
eq(incominginvoices.id, invoiceId),
|
||||
eq(incominginvoices.tenant, tenantId)
|
||||
)).limit(1),
|
||||
server.db.select().from(bankstatements).where(and(
|
||||
eq(bankstatements.id, statementId),
|
||||
eq(bankstatements.tenant, tenantId)
|
||||
)).limit(1),
|
||||
server.db.select({ amount: statementallocations.amount }).from(statementallocations).where(and(
|
||||
eq(statementallocations.incominginvoice, invoiceId),
|
||||
eq(statementallocations.tenant, tenantId),
|
||||
eq(statementallocations.archived, false)
|
||||
)),
|
||||
server.db.select({ amount: statementallocations.amount }).from(statementallocations).where(and(
|
||||
eq(statementallocations.bankstatement, statementId),
|
||||
eq(statementallocations.tenant, tenantId),
|
||||
eq(statementallocations.archived, false)
|
||||
)),
|
||||
])
|
||||
|
||||
if (!invoice || !statement) return allocation
|
||||
|
||||
const accountTotals = (invoice.accounts as any[] || []).reduce((totals, account) => ({
|
||||
net: totals.net + Number(account.amountNet || 0),
|
||||
tax: totals.tax + Number(account.amountTax || 0),
|
||||
}), { net: 0, tax: 0 })
|
||||
const correctedTax = invoice.taxAmountOverride !== null && invoice.taxAmountOverride !== undefined && invoice.taxAmountOverride !== ""
|
||||
? Number(invoice.taxAmountOverride)
|
||||
: accountTotals.tax
|
||||
const legacyRemaining = Math.max(0, Math.abs(accountTotals.net + accountTotals.tax)
|
||||
- invoiceAllocations.reduce((sum, item) => sum + Math.abs(Number(item.amount || 0)), 0))
|
||||
const correctedRemaining = Math.max(0, Math.abs(accountTotals.net + correctedTax)
|
||||
- invoiceAllocations.reduce((sum, item) => sum + Math.abs(Number(item.amount || 0)), 0))
|
||||
const statementRemaining = Math.max(0, Math.abs(Number(statement.amount || 0))
|
||||
- statementAllocations.reduce((sum, item) => sum + Math.abs(Number(item.amount || 0)), 0))
|
||||
const requestedAmount = Number(allocation.amount || 0)
|
||||
const legacyAssignment = Math.min(legacyRemaining, statementRemaining)
|
||||
|
||||
// Nur den automatisch vorgeschlagenen Altbetrag ersetzen. Bewusst eingegebene Teilbeträge bleiben erhalten.
|
||||
if (Math.abs(Math.abs(requestedAmount) - legacyAssignment) >= 0.005) return allocation
|
||||
|
||||
const currentAssignment = Math.min(correctedRemaining, statementRemaining)
|
||||
return {
|
||||
...allocation,
|
||||
amount: Number((Math.sign(requestedAmount || (invoice.expense ? -1 : 1)) * currentAssignment).toFixed(2)),
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeManualSide = (payload: any, keys: string[]) =>
|
||||
keys.filter((key) => payload[key] !== null && payload[key] !== undefined && payload[key] !== "")
|
||||
|
||||
@@ -1108,6 +1162,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
const { data: payload } = req.body as { data: any }
|
||||
const prepared = prepareStatementAllocationPayload(payload)
|
||||
if (prepared.error) return reply.code(400).send({ error: prepared.error })
|
||||
prepared.data = await useCurrentIncomingInvoiceAmount(req.user.tenant_id, prepared.data)
|
||||
|
||||
const inserted = await server.db.insert(statementallocations).values({
|
||||
...prepared.data,
|
||||
|
||||
@@ -80,6 +80,19 @@ const formatDatevDate = (date: dayjs.ConfigType, format: string) => {
|
||||
return parsed.isValid() ? parsed.tz(DATEV_TIMEZONE).format(format) : "";
|
||||
};
|
||||
|
||||
const getIncomingInvoiceTaxOverride = (invoice: any) => {
|
||||
if (invoice.taxAmountOverride === null || invoice.taxAmountOverride === undefined || invoice.taxAmountOverride === "") return null;
|
||||
|
||||
const calculatedTax = (invoice.accounts as any[] || []).reduce((sum, account) => sum + Number(account.amountTax || 0), 0);
|
||||
const overriddenTax = Number(invoice.taxAmountOverride);
|
||||
if (!Number.isFinite(overriddenTax) || Math.abs(overriddenTax - calculatedTax) < 0.005) return null;
|
||||
|
||||
return {
|
||||
amount: overriddenTax,
|
||||
correction: Number((overriddenTax - calculatedTax).toFixed(2)),
|
||||
};
|
||||
};
|
||||
|
||||
const getCreatedDocumentRevenueLines = (document: any) => {
|
||||
const totals = getCreatedDocumentTotal(document);
|
||||
|
||||
@@ -354,7 +367,12 @@ export async function buildExportZip(
|
||||
// ER
|
||||
incominginvoicesList.forEach(ii => {
|
||||
const accs = ii.accounts as any[] || [];
|
||||
accs.forEach(account => {
|
||||
const taxOverride = getIncomingInvoiceTaxOverride(ii);
|
||||
const correctionAccountIndex = taxOverride
|
||||
? Math.max(0, accs.findIndex(account => account.taxType === "19" || account.taxType === "7"))
|
||||
: -1;
|
||||
|
||||
accs.forEach((account, accountIndex) => {
|
||||
let file = filesIncomingInvoices.find(i => i.incominginvoice === ii.id);
|
||||
let accountData = accountsList.find(i => i.id === account.account);
|
||||
if (!accountData) return;
|
||||
@@ -367,9 +385,13 @@ export async function buildExportZip(
|
||||
else if(account.taxType === '7I') buschluessel = "18";
|
||||
else buschluessel = "-";
|
||||
|
||||
let amountGross =/* account.amountGross ? account.amountGross : */(account.amountNet || 0) + (account.amountTax || 0);
|
||||
let amountGross = Number(account.amountNet || 0) + Number(account.amountTax || 0);
|
||||
if (taxOverride && accountIndex === correctionAccountIndex) amountGross += taxOverride.correction;
|
||||
let shSelector = Math.sign(amountGross) === -1 ? "H" : "S";
|
||||
let text = `ER ${ii.reference}: ${escapeString(ii.description)}`.substring(0,59);
|
||||
const taxReview = taxOverride && accountIndex === correctionAccountIndex
|
||||
? `USt pruefen ${displayCurrency(taxOverride.amount, true)} - `
|
||||
: "";
|
||||
let text = `${taxReview}ER ${ii.reference}: ${escapeString(ii.description)}`.substring(0,59);
|
||||
const vend = ii.vendor; // durch Mapping verfügbar
|
||||
|
||||
bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${vend?.vendorNumber || ""};"${buschluessel}";${formatDatevDate(ii.date, "DDMM")};"${ii.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${vend?.name || ""}";"Kundennummer";"${vend?.vendorNumber || ""}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${formatDatevDate(ii.date, "DD.MM.YYYY")}";"Belegdatum";"${formatDatevDate(ii.date, "DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`);
|
||||
|
||||
@@ -287,6 +287,12 @@ const getIncomingInvoiceTaxBreakdown = (invoice: any): TaxBreakdown => {
|
||||
}
|
||||
});
|
||||
|
||||
if (invoice?.taxAmountOverride !== null && invoice?.taxAmountOverride !== undefined && invoice?.taxAmountOverride !== "") {
|
||||
const correction = Number(invoice.taxAmountOverride) - breakdown.tax19 - breakdown.tax7;
|
||||
if (breakdown.tax19 !== 0 || breakdown.net19 !== 0) breakdown.tax19 += correction;
|
||||
else if (breakdown.tax7 !== 0 || breakdown.net7 !== 0) breakdown.tax7 += correction;
|
||||
}
|
||||
|
||||
return {
|
||||
net19: roundMoney(breakdown.net19),
|
||||
tax19: roundMoney(breakdown.tax19),
|
||||
@@ -297,9 +303,14 @@ const getIncomingInvoiceTaxBreakdown = (invoice: any): TaxBreakdown => {
|
||||
};
|
||||
|
||||
const getIncomingInvoiceSignedAmount = (invoice: any) => {
|
||||
const amount = (invoice.accounts || []).reduce((sum: number, account: any) => {
|
||||
return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0);
|
||||
}, 0);
|
||||
const totals = (invoice.accounts || []).reduce((result: { net: number, tax: number }, account: any) => ({
|
||||
net: result.net + Number(account.amountNet || 0),
|
||||
tax: result.tax + Number(account.amountTax || 0),
|
||||
}), { net: 0, tax: 0 });
|
||||
const tax = invoice.taxAmountOverride !== null && invoice.taxAmountOverride !== undefined && invoice.taxAmountOverride !== ""
|
||||
? Number(invoice.taxAmountOverride)
|
||||
: totals.tax;
|
||||
const amount = totals.net + tax;
|
||||
|
||||
return roundMoney(invoice.expense === false ? amount : amount * -1);
|
||||
};
|
||||
|
||||
@@ -152,7 +152,7 @@ setup()
|
||||
<div class="flex flex-row">
|
||||
<div class="w-1/3">
|
||||
<PDFViewer
|
||||
v-if="props.documentData.id && props.documentData.path.toLowerCase().includes('pdf')"
|
||||
v-if="props.documentData.id && String(props.documentData.path || '').toLowerCase().includes('pdf')"
|
||||
:file-id="props.documentData.id" />
|
||||
|
||||
<img
|
||||
|
||||
@@ -6,9 +6,13 @@ let unallocatedStatements = ref(0)
|
||||
let bankaccounts = ref([])
|
||||
|
||||
const setupPage = async () => {
|
||||
let bankstatements = (await useEntities("bankstatements").select("*, statementallocations(*)","date",true)).filter(i => !i.archived)
|
||||
const [statementItems, accountItems] = await Promise.all([
|
||||
useEntities("bankstatements").select("*, statementallocations(*)", "date", true),
|
||||
useEntities("bankaccounts").select()
|
||||
])
|
||||
let bankstatements = statementItems.filter(i => !i.archived)
|
||||
unallocatedStatements.value = bankstatements.filter(i => Number(calculateOpenSum(i)) !== 0).length
|
||||
bankaccounts.value = await useEntities("bankaccounts").select()
|
||||
bankaccounts.value = accountItems
|
||||
}
|
||||
|
||||
setupPage()
|
||||
|
||||
@@ -10,9 +10,11 @@ let draftInvoicesSum = ref(0)
|
||||
let draftInvoicesCount = ref(0)
|
||||
|
||||
let countPreparedOpenIncomingInvoices = ref(0)
|
||||
const { loadCoreData } = useDashboardData()
|
||||
|
||||
const setupPage = async () => {
|
||||
let items = (await useEntities("createddocuments").select("*, statementallocations(*), customer(id,name), linkedDocument(*)")).filter(i => !i.archived)
|
||||
const { createdDocuments, incomingInvoices } = await loadCoreData()
|
||||
let items = createdDocuments.filter(i => !i.archived)
|
||||
let documents = items.filter(i => i.type === "invoices" ||i.type === "advanceInvoices")
|
||||
|
||||
let draftDocuments = documents.filter(i => i.state === "Entwurf")
|
||||
@@ -36,7 +38,7 @@ const setupPage = async () => {
|
||||
})
|
||||
draftInvoicesCount.value = draftDocuments.length
|
||||
|
||||
countPreparedOpenIncomingInvoices.value = (await useEntities("incominginvoices").select("id, state")).filter(i => i.state === "Vorbereitet" && !i.archived).length
|
||||
countPreparedOpenIncomingInvoices.value = incomingInvoices.filter(i => i.state === "Vorbereitet" && !i.archived).length
|
||||
}
|
||||
|
||||
setupPage()
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
formatTaxEvaluationPeriodRange,
|
||||
getCreatedDocumentTaxBreakdown,
|
||||
getIncomingInvoiceTaxBreakdown,
|
||||
getManualBookingTaxBreakdown,
|
||||
getTaxEvaluationPeriodBounds,
|
||||
normalizeTaxEvaluationPeriod
|
||||
} from "~/composables/useTaxEvaluation"
|
||||
@@ -40,7 +41,10 @@ const loadSummary = async () => {
|
||||
const periodType = normalizeTaxEvaluationPeriod(auth.activeTenantData?.taxEvaluationPeriod)
|
||||
const bounds = getTaxEvaluationPeriodBounds(dayjs(), periodType)
|
||||
|
||||
const { createdDocuments: docs, incomingInvoices: incoming } = await loadCoreData()
|
||||
const [{ createdDocuments: docs, incomingInvoices: incoming }, manualBookings] = await Promise.all([
|
||||
loadCoreData(),
|
||||
useNuxtApp().$api("/api/banking/manual-bookings") as Promise<any[]>
|
||||
])
|
||||
|
||||
const outputDocs = (docs || []).filter((doc: any) => {
|
||||
if (doc?.state !== "Gebucht") return false
|
||||
@@ -67,12 +71,28 @@ const loadSummary = async () => {
|
||||
return sum + breakdown.tax19 + breakdown.tax7
|
||||
}, 0)
|
||||
|
||||
const manualTax = (manualBookings || [])
|
||||
.filter((booking: any) => {
|
||||
const date = dayjs(booking.manualBookingDate)
|
||||
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||
})
|
||||
.reduce((sum: { outputTax19: number; inputTax19: number }, booking: any) => {
|
||||
const breakdown = getManualBookingTaxBreakdown(booking)
|
||||
return {
|
||||
outputTax19: sum.outputTax19 + breakdown.outputTax19,
|
||||
inputTax19: sum.inputTax19 + breakdown.inputTax19,
|
||||
}
|
||||
}, { outputTax19: 0, inputTax19: 0 })
|
||||
|
||||
const totalOutputTax = outputTax + manualTax.outputTax19
|
||||
const totalInputTax = inputTax + manualTax.inputTax19
|
||||
|
||||
summary.value = {
|
||||
label: formatTaxEvaluationPeriodLabel(bounds.start, periodType),
|
||||
range: formatTaxEvaluationPeriodRange(bounds.start, periodType),
|
||||
outputTax: Number(outputTax.toFixed(2)),
|
||||
inputTax: Number(inputTax.toFixed(2)),
|
||||
balance: Number((outputTax - inputTax).toFixed(2)),
|
||||
outputTax: Number(totalOutputTax.toFixed(2)),
|
||||
inputTax: Number(totalInputTax.toFixed(2)),
|
||||
balance: Number((totalOutputTax - totalInputTax).toFixed(2)),
|
||||
outputCount: outputDocs.length,
|
||||
inputCount: inputDocs.length,
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useDashboardData = () => {
|
||||
if (!force && cache.pendingRequest) return cache.pendingRequest
|
||||
|
||||
cache.pendingRequest = Promise.all([
|
||||
useEntities("createddocuments").select(),
|
||||
useEntities("createddocuments").select("*, statementallocations(*), customer(id,name), linkedDocument(*)"),
|
||||
useEntities("incominginvoices").select()
|
||||
])
|
||||
.then(([createdDocuments, incomingInvoices]) => {
|
||||
|
||||
@@ -222,13 +222,24 @@ export const getIncomingInvoiceImmediateExpenseNet = (invoice: any) => {
|
||||
}
|
||||
|
||||
export const getIncomingInvoiceImmediateExpenseGross = (invoice: any) => {
|
||||
return Number(((invoice?.accounts || []).reduce((sum: number, account: any) => {
|
||||
const immediateAccounts = (invoice?.accounts || []).filter((account: any) => {
|
||||
const normalized = normalizeIncomingInvoiceAccount(account, invoice?.date)
|
||||
return !isDepreciationBookingMode(normalized.bookingMode)
|
||||
})
|
||||
const gross = immediateAccounts.reduce((sum: number, account: any) => {
|
||||
const normalized = normalizeIncomingInvoiceAccount(account, invoice?.date)
|
||||
if (isDepreciationBookingMode(normalized.bookingMode)) return sum
|
||||
|
||||
const amountGross = Number(normalized.amountGross)
|
||||
return sum + (Number.isFinite(amountGross) ? amountGross : Number(normalized.amountNet || 0) + Number(normalized.amountTax || 0))
|
||||
}, 0)).toFixed(2))
|
||||
}, 0)
|
||||
const calculatedTax = (invoice?.accounts || []).reduce((sum: number, account: any) => sum + Number(account.amountTax || 0), 0)
|
||||
const correction = immediateAccounts.length > 0
|
||||
&& invoice?.taxAmountOverride !== null
|
||||
&& invoice?.taxAmountOverride !== undefined
|
||||
&& invoice?.taxAmountOverride !== ""
|
||||
? Number(invoice.taxAmountOverride) - calculatedTax
|
||||
: 0
|
||||
|
||||
return Number((gross + correction).toFixed(2))
|
||||
}
|
||||
|
||||
export const getIncomingInvoiceDepreciationRows = (invoice: any, rangeStart: any, rangeEnd: any) => {
|
||||
|
||||
@@ -77,7 +77,10 @@ export const useFiles = () => {
|
||||
const selectDocument = async (id) => {
|
||||
let documentIds = [id]
|
||||
if(documentIds.length === 0) return []
|
||||
const fileData = await useEntities("files").selectSingle(id)
|
||||
const fileData = await useEntities("files").selectSingle(
|
||||
id,
|
||||
"*, incominginvoice(*), project(*), vendor(*), customer(*), contract(*), plant(*), createddocument(*), vehicle(*), product(*), profile(*), check(*), inventoryitem(*)"
|
||||
)
|
||||
const res = await useNuxtApp().$api("/api/files/presigned",{
|
||||
method: "POST",
|
||||
body: {
|
||||
|
||||
@@ -12,17 +12,15 @@ export const useSum = () => {
|
||||
}
|
||||
|
||||
const getIncomingInvoiceSum = (invoice) => {
|
||||
let sum = 0
|
||||
invoice.accounts.forEach(account => {
|
||||
const totals = (invoice.accounts || []).reduce((result, account) => ({
|
||||
net: result.net + Number(account.amountNet || 0),
|
||||
tax: result.tax + Number(account.amountTax || 0)
|
||||
}), { net: 0, tax: 0 })
|
||||
const tax = invoice.taxAmountOverride !== null && invoice.taxAmountOverride !== undefined && invoice.taxAmountOverride !== ""
|
||||
? Number(invoice.taxAmountOverride)
|
||||
: totals.tax
|
||||
|
||||
|
||||
sum += account.amountTax
|
||||
sum += account.amountNet
|
||||
|
||||
|
||||
|
||||
})
|
||||
return sum.toFixed(2)
|
||||
return (totals.net + tax).toFixed(2)
|
||||
}
|
||||
|
||||
const getCreatedDocumentSum = (createddocument,createddocuments = []) => {
|
||||
|
||||
@@ -152,6 +152,12 @@ export const getIncomingInvoiceTaxBreakdown = (invoice: any) => {
|
||||
}
|
||||
})
|
||||
|
||||
if (invoice?.taxAmountOverride !== null && invoice?.taxAmountOverride !== undefined && invoice?.taxAmountOverride !== "") {
|
||||
const correction = Number(invoice.taxAmountOverride) - breakdown.tax19 - breakdown.tax7
|
||||
if (breakdown.tax19 !== 0 || breakdown.net19 !== 0) breakdown.tax19 += correction
|
||||
else if (breakdown.tax7 !== 0 || breakdown.net7 !== 0) breakdown.tax7 += correction
|
||||
}
|
||||
|
||||
return {
|
||||
net19: Number(breakdown.net19.toFixed(2)),
|
||||
tax19: Number(breakdown.tax19.toFixed(2)),
|
||||
@@ -160,3 +166,33 @@ export const getIncomingInvoiceTaxBreakdown = (invoice: any) => {
|
||||
net0: Number(breakdown.net0.toFixed(2)),
|
||||
}
|
||||
}
|
||||
|
||||
const getSkr03AccountNumber = (account: any) => {
|
||||
if (!account || String(account.accountChart || "").toLowerCase() !== "skr03") return null
|
||||
return String(account.number || "")
|
||||
}
|
||||
|
||||
export const getManualBookingTaxBreakdown = (booking: any) => {
|
||||
const amount = Number(booking?.amount || 0)
|
||||
const breakdown = { outputTax19: 0, inputTax19: 0 }
|
||||
|
||||
if (!Number.isFinite(amount) || amount === 0) return breakdown
|
||||
|
||||
const applySide = (account: any, side: "debit" | "credit") => {
|
||||
const accountNumber = getSkr03AccountNumber(account)
|
||||
|
||||
if (accountNumber === "1576") {
|
||||
breakdown.inputTax19 += side === "debit" ? amount : -amount
|
||||
} else if (accountNumber === "1776") {
|
||||
breakdown.outputTax19 += side === "credit" ? amount : -amount
|
||||
}
|
||||
}
|
||||
|
||||
applySide(booking.account, "debit")
|
||||
applySide(booking.contraAccount, "credit")
|
||||
|
||||
return {
|
||||
outputTax19: Number(breakdown.outputTax19.toFixed(2)),
|
||||
inputTax19: Number(breakdown.inputTax19.toFixed(2)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,7 @@ const currentBalance = computed(() => {
|
||||
})
|
||||
|
||||
const getIncomingInvoiceGross = (invoice) => {
|
||||
return Number((invoice.accounts || []).reduce((sum, account) => {
|
||||
return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0)
|
||||
}, 0))
|
||||
return Number(useSum().getIncomingInvoiceSum(invoice))
|
||||
}
|
||||
|
||||
const getIncomingInvoiceOpenAmount = (invoice) => {
|
||||
|
||||
@@ -68,9 +68,7 @@ const buildEntries = (rows, type, labelBuilder) =>
|
||||
}))
|
||||
|
||||
const getIncomingInvoiceGross = (invoice) => {
|
||||
return Number((invoice.accounts || []).reduce((sum, account) => {
|
||||
return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0)
|
||||
}, 0))
|
||||
return Number(useSum().getIncomingInvoiceSum(invoice))
|
||||
}
|
||||
|
||||
const getIncomingInvoiceOpenAmount = (invoice) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
formatTaxEvaluationPeriodRange,
|
||||
getCreatedDocumentTaxBreakdown,
|
||||
getIncomingInvoiceTaxBreakdown,
|
||||
getManualBookingTaxBreakdown,
|
||||
getTaxEvaluationPeriodBounds,
|
||||
normalizeTaxEvaluationPeriod,
|
||||
shiftTaxEvaluationPeriodStart
|
||||
@@ -18,6 +19,7 @@ const auth = useAuthStore()
|
||||
const loading = ref(true)
|
||||
const createdDocuments = ref<any[]>([])
|
||||
const incomingInvoices = ref<any[]>([])
|
||||
const manualBookings = ref<any[]>([])
|
||||
|
||||
const periodType = computed(() => normalizeTaxEvaluationPeriod(auth.activeTenantData?.taxEvaluationPeriod))
|
||||
|
||||
@@ -40,13 +42,15 @@ const loadData = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const [docs, incoming] = await Promise.all([
|
||||
const [docs, incoming, manual] = await Promise.all([
|
||||
useEntities("createddocuments").select(),
|
||||
useEntities("incominginvoices").select()
|
||||
useEntities("incominginvoices").select(),
|
||||
useNuxtApp().$api("/api/banking/manual-bookings")
|
||||
])
|
||||
|
||||
createdDocuments.value = (docs || []).filter(isRelevantOutputDocument)
|
||||
incomingInvoices.value = (incoming || []).filter(isRelevantInputInvoice)
|
||||
manualBookings.value = (manual as any[]) || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -69,6 +73,19 @@ const periods = computed(() => {
|
||||
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||
})
|
||||
|
||||
const manualTax = manualBookings.value
|
||||
.filter((booking) => {
|
||||
const date = dayjs(booking.manualBookingDate)
|
||||
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||
})
|
||||
.reduce((sum, booking) => {
|
||||
const breakdown = getManualBookingTaxBreakdown(booking)
|
||||
return {
|
||||
outputTax19: sum.outputTax19 + breakdown.outputTax19,
|
||||
inputTax19: sum.inputTax19 + breakdown.inputTax19,
|
||||
}
|
||||
}, { outputTax19: 0, inputTax19: 0 })
|
||||
|
||||
const output = outputDocs.reduce((sum, doc) => {
|
||||
const breakdown = getCreatedDocumentTaxBreakdown(doc)
|
||||
return {
|
||||
@@ -91,6 +108,9 @@ const periods = computed(() => {
|
||||
}
|
||||
}, { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 })
|
||||
|
||||
output.tax19 = Number((output.tax19 + manualTax.outputTax19).toFixed(2))
|
||||
input.tax19 = Number((input.tax19 + manualTax.inputTax19).toFixed(2))
|
||||
|
||||
const outputTax = Number((output.tax19 + output.tax7).toFixed(2))
|
||||
const inputTax = Number((input.tax19 + input.tax7).toFixed(2))
|
||||
const balance = Number((outputTax - inputTax).toFixed(2))
|
||||
@@ -146,7 +166,7 @@ onMounted(loadData)
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Intervall: {{ periodType === "monthly" ? "monatlich" : periodType === "quarterly" ? "quartalsweise" : "jährlich" }}.
|
||||
Berücksichtigt werden gebuchte Ausgangsrechnungen, Abschlags- und Stornorechnungen sowie gebuchte Eingangsbelege mit Datum.
|
||||
Berücksichtigt werden gebuchte Ausgangsrechnungen, Abschlags- und Stornorechnungen, gebuchte Eingangsbelege sowie manuelle Buchungen auf SKR03 1576 und 1776.
|
||||
</p>
|
||||
<p v-if="currentPeriod" class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ currentPeriod.range }}
|
||||
|
||||
@@ -230,13 +230,7 @@ const calculateOpenSum = (statement) => {
|
||||
}
|
||||
|
||||
const getInvoiceSum = (invoice, onlyOpenSum) => {
|
||||
let sum = 0
|
||||
if (invoice.accounts) {
|
||||
invoice.accounts.forEach(account => {
|
||||
sum += (account.amountTax || 0)
|
||||
sum += (account.amountNet || 0)
|
||||
})
|
||||
}
|
||||
let sum = Number(useSum().getIncomingInvoiceSum(invoice))
|
||||
|
||||
if (onlyOpenSum) sum = sum + Number(invoice.statementallocations.reduce((n, {amount}) => n + amount, 0))
|
||||
|
||||
@@ -771,8 +765,16 @@ onMounted(() => {
|
||||
<td class="p-4 truncate max-w-[180px] font-medium">
|
||||
{{ row.amount < 0 ? row.credName : row.debName }}
|
||||
</td>
|
||||
<td class="p-4 text-gray-500 truncate max-w-[350px] text-xs">
|
||||
{{ row.text }}
|
||||
<td class="p-4 text-gray-500 max-w-[350px] text-xs">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<UTooltip v-if="String(row.notes || '').trim()" text="Notiz hinterlegt">
|
||||
<UIcon
|
||||
name="i-heroicons-chat-bubble-left-ellipsis"
|
||||
class="w-4 h-4 shrink-0 text-primary-500"
|
||||
/>
|
||||
</UTooltip>
|
||||
<span class="truncate">{{ row.text }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
@@ -40,6 +40,9 @@ const ownaccounts = ref([])
|
||||
const loading = ref(true)
|
||||
const loadingDocuments = ref(true)
|
||||
const savingNotes = ref(false)
|
||||
const notesSaveDelay = 800
|
||||
let notesSaveTimer = null
|
||||
let notesSaveQueued = false
|
||||
|
||||
const rebuildDocumentLists = () => {
|
||||
const documents = createddocuments.value.filter(i => i.type === "invoices" || i.type === "advanceInvoices")
|
||||
@@ -127,13 +130,7 @@ const separateIBAN = (input) => {
|
||||
}
|
||||
|
||||
const getInvoiceSum = (invoice, onlyOpenSum) => {
|
||||
let sum = 0
|
||||
if (invoice.accounts) {
|
||||
invoice.accounts.forEach(account => {
|
||||
sum += (account.amountTax || 0)
|
||||
sum += (account.amountNet || 0)
|
||||
})
|
||||
}
|
||||
let sum = Number(useSum().getIncomingInvoiceSum(invoice))
|
||||
|
||||
if (onlyOpenSum) sum = sum + Number(invoice.statementallocations.reduce((n, {amount}) => n + amount, 0))
|
||||
|
||||
@@ -262,14 +259,26 @@ const removeAllocation = async (allocationId) => {
|
||||
}
|
||||
|
||||
const saveNotes = async () => {
|
||||
if (!itemInfo.value?.id || !notesChanged.value) return
|
||||
if (savingNotes.value) {
|
||||
notesSaveQueued = true
|
||||
return
|
||||
}
|
||||
|
||||
const notes = String(itemInfo.value.notes || "").trim() || null
|
||||
savingNotes.value = true
|
||||
try {
|
||||
const notes = String(itemInfo.value.notes || "").trim() || null
|
||||
const updated = await useEntities("bankstatements").update(itemInfo.value.id, {notes}, true)
|
||||
itemInfo.value.notes = updated.notes
|
||||
oldItemInfo.value.notes = updated.notes
|
||||
if (String(itemInfo.value.notes || "").trim() === String(notes || "")) {
|
||||
itemInfo.value.notes = updated.notes
|
||||
}
|
||||
} finally {
|
||||
savingNotes.value = false
|
||||
if (notesSaveQueued || notesChanged.value) {
|
||||
notesSaveQueued = false
|
||||
scheduleNotesSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +286,26 @@ const notesChanged = computed(() =>
|
||||
String(itemInfo.value?.notes || "").trim() !== String(oldItemInfo.value?.notes || "").trim()
|
||||
)
|
||||
|
||||
const scheduleNotesSave = () => {
|
||||
if (notesSaveTimer) clearTimeout(notesSaveTimer)
|
||||
notesSaveTimer = setTimeout(() => {
|
||||
notesSaveTimer = null
|
||||
void saveNotes()
|
||||
}, notesSaveDelay)
|
||||
}
|
||||
|
||||
watch(() => itemInfo.value?.notes, () => {
|
||||
if (!loading.value && notesChanged.value) scheduleNotesSave()
|
||||
})
|
||||
|
||||
onBeforeRouteLeave(async () => {
|
||||
if (notesSaveTimer) {
|
||||
clearTimeout(notesSaveTimer)
|
||||
notesSaveTimer = null
|
||||
}
|
||||
if (notesChanged.value) await saveNotes()
|
||||
})
|
||||
|
||||
const searchString = ref(tempStore.searchStrings["bankstatementsedit"] || '')
|
||||
|
||||
const clearSearchString = () => {
|
||||
@@ -616,22 +645,12 @@ setup()
|
||||
<UFormField label="Notiz" size="sm">
|
||||
<UTextarea
|
||||
v-model="itemInfo.notes"
|
||||
class="w-full"
|
||||
:rows="3"
|
||||
autoresize
|
||||
placeholder="Interne Notiz zu dieser Bankbuchung hinzufügen …"
|
||||
/>
|
||||
</UFormField>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<UButton
|
||||
icon="i-heroicons-check"
|
||||
size="sm"
|
||||
:disabled="!notesChanged"
|
||||
:loading="savingNotes"
|
||||
@click="saveNotes"
|
||||
>
|
||||
Notiz speichern
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
|
||||
@@ -63,20 +63,24 @@ watch(searchString, (val) => {
|
||||
const setupPage = async () => {
|
||||
loadingDocs.value = true
|
||||
try {
|
||||
const [fRes, dRes, tRes] = await Promise.all([
|
||||
const [fRes, dRes] = await Promise.all([
|
||||
useEntities("folders").select(),
|
||||
files.selectDocuments(),
|
||||
useEntities("filetags").select()
|
||||
useEntities("files").select("id, path, folder, type, createdAt")
|
||||
])
|
||||
folders.value = fRes || []
|
||||
documents.value = dRes || []
|
||||
filetags.value = tRes || []
|
||||
|
||||
syncCurrentFolderFromRoute()
|
||||
} finally {
|
||||
loadingDocs.value = false
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
useEntities("filetags").select()
|
||||
.then((items) => {
|
||||
filetags.value = items || []
|
||||
})
|
||||
.catch((error) => console.error("Dateitypen konnten nicht geladen werden:", error))
|
||||
}
|
||||
|
||||
// --- Global Drag & Drop (Auto-Open Upload Modal) ---
|
||||
@@ -231,6 +235,11 @@ const breadcrumbItems = computed(() => {
|
||||
})
|
||||
|
||||
// --- Data Mapping ---
|
||||
const getFileName = (file) => {
|
||||
const path = String(file?.path || '').trim()
|
||||
return path ? path.split('/').pop() : `Datei ${file?.id || ''}`.trim()
|
||||
}
|
||||
|
||||
const renderedFileList = computed(() => {
|
||||
const folderList = folders.value
|
||||
.filter(i => currentFolder.value ? i.parent === currentFolder.value.id : !i.parent)
|
||||
@@ -239,7 +248,7 @@ const renderedFileList = computed(() => {
|
||||
|
||||
const fileList = documents.value
|
||||
.filter(i => currentFolder.value ? i.folder === currentFolder.value.id : !i.folder)
|
||||
.map(i => ({...i, label: i.path.split("/").pop(), type: "file"}))
|
||||
.map(i => ({...i, label: getFileName(i), type: "file"}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
||||
let combined = [...folderList, ...fileList]
|
||||
@@ -311,7 +320,7 @@ const updateName = async () => {
|
||||
await useEntities("folders").update(renameData.value.id, {name: renameData.value.name})
|
||||
} else {
|
||||
const file = documents.value.find(d => d.id === renameData.value.id)
|
||||
const pathParts = file.path.split('/')
|
||||
const pathParts = String(file?.path || '').split('/')
|
||||
pathParts[pathParts.length - 1] = renameData.value.name
|
||||
await useEntities("files").update(renameData.value.id, {path: pathParts.join('/')})
|
||||
}
|
||||
@@ -323,11 +332,22 @@ const updateName = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const showFile = (fileId) => {
|
||||
modal.open(DocumentDisplayModal, {
|
||||
documentData: documents.value.find(i => i.id === fileId),
|
||||
onUpdatedNeeded: () => setupPage()
|
||||
})
|
||||
const showFile = async (fileId) => {
|
||||
loadingDocs.value = true
|
||||
try {
|
||||
const documentData = await files.selectDocument(fileId)
|
||||
if (!documentData) throw new Error("Datei nicht gefunden")
|
||||
|
||||
modal.open(DocumentDisplayModal, {
|
||||
documentData,
|
||||
onUpdatedNeeded: () => setupPage()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast.add({title: 'Datei konnte nicht geöffnet werden', color: 'red'})
|
||||
} finally {
|
||||
loadingDocs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openScanModal = () => {
|
||||
|
||||
@@ -49,6 +49,11 @@ const loadedFileId = ref(null)
|
||||
const invoiceFiles = ref([])
|
||||
const paymentTypeItems = ['Überweisung', 'Lastschrift', 'Kreditkarte', 'PayPal', 'Bar', 'Sonstiges']
|
||||
const files = useFiles()
|
||||
const hasActiveBankAssignmentIn = (allocations = []) => allocations.some((allocation) => {
|
||||
if (allocation?.archived) return false
|
||||
|
||||
return Boolean(allocation?.bankstatement || allocation?.bs_id)
|
||||
})
|
||||
|
||||
const setup = async () => {
|
||||
// 1. Daten laden
|
||||
@@ -65,6 +70,18 @@ const setup = async () => {
|
||||
accounts: normalizeIncomingInvoiceAccounts(invoiceData.accounts || [], invoiceData.date)
|
||||
}
|
||||
|
||||
if (mode.value === "edit" && invoiceData.state === "Gebucht" && (invoiceData.archived || hasActiveBankAssignmentIn(invoiceData.statementallocations))) {
|
||||
toast.add({
|
||||
title: "Bearbeiten nicht möglich",
|
||||
description: invoiceData.archived
|
||||
? "Archivierte Eingangsbelege können nicht bearbeitet werden."
|
||||
: "Der Eingangsbeleg ist bereits einer Bankbuchung zugewiesen.",
|
||||
color: "error"
|
||||
})
|
||||
await navigateTo(`/incomingInvoices/show/${invoiceData.id}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback Accounts
|
||||
if(itemInfo.value.accounts.length === 0) {
|
||||
itemInfo.value.accounts.push(createIncomingInvoiceAccount({ depreciationStartDate: itemInfo.value.date || null }))
|
||||
@@ -103,6 +120,8 @@ watch(() => itemInfo.value.date, (value) => {
|
||||
|
||||
// --- Berechnungslogik ---
|
||||
const useNetMode = ref(false)
|
||||
const editingTotalTax = ref(false)
|
||||
const totalTaxDraft = ref(null)
|
||||
|
||||
const taxOptions = ref([
|
||||
{ label: "19% USt", percentage: 19, key: "19" },
|
||||
@@ -148,6 +167,20 @@ const bankBookingDateLabel = computed(() => {
|
||||
|
||||
return bankBookingDates.value.map(formatDate).join(", ")
|
||||
})
|
||||
const hasActiveBankAssignment = computed(() => hasActiveBankAssignmentIn(itemInfo.value.statementallocations))
|
||||
const isBookedIncomingInvoice = computed(() => itemInfo.value.state === "Gebucht")
|
||||
const canEditIncomingInvoice = computed(() => (
|
||||
mode.value === "show"
|
||||
&& isBookedIncomingInvoice.value
|
||||
&& !itemInfo.value.archived
|
||||
&& !hasActiveBankAssignment.value
|
||||
))
|
||||
const canArchiveIncomingInvoice = computed(() => {
|
||||
if (itemInfo.value.archived) return false
|
||||
if (isBookedIncomingInvoice.value) return !hasActiveBankAssignment.value
|
||||
|
||||
return mode.value !== "show"
|
||||
})
|
||||
const vendorName = computed(() => vendors.value.find((vendor) => vendor.id === itemInfo.value.vendor)?.name || "-")
|
||||
const eInvoiceValidation = computed(() => itemInfo.value.eInvoiceValidation || null)
|
||||
const eInvoiceSourceLabel = computed(() => {
|
||||
@@ -186,9 +219,12 @@ const totalCalculated = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
totalGross = Number(totalNet + totalAmount19Tax + totalAmount7Tax)
|
||||
const calculatedTax = Number(totalAmount19Tax + totalAmount7Tax)
|
||||
const hasTaxOverride = itemInfo.value.taxAmountOverride !== null && itemInfo.value.taxAmountOverride !== undefined && itemInfo.value.taxAmountOverride !== ""
|
||||
const totalTax = hasTaxOverride ? Number(itemInfo.value.taxAmountOverride) : calculatedTax
|
||||
totalGross = Number(totalNet + totalTax)
|
||||
|
||||
return { totalNet, totalAmount19Tax, totalAmount7Tax, totalGross }
|
||||
return { totalNet, totalAmount19Tax, totalAmount7Tax, calculatedTax, totalTax, totalGross, hasTaxOverride }
|
||||
})
|
||||
|
||||
const hasAmount = (value) => value !== null && value !== undefined && value !== ""
|
||||
@@ -228,9 +264,24 @@ const moveGrossToNet = (item) => {
|
||||
recalculateItem(item, 'net')
|
||||
}
|
||||
|
||||
const startEditingTotalTax = () => {
|
||||
totalTaxDraft.value = totalCalculated.value.totalTax.toFixed(2)
|
||||
editingTotalTax.value = true
|
||||
}
|
||||
|
||||
const applyTotalTaxOverride = () => {
|
||||
if (!hasValidNumber(totalTaxDraft.value)) return
|
||||
|
||||
const value = Number(Number(totalTaxDraft.value).toFixed(2))
|
||||
itemInfo.value.taxAmountOverride = Math.abs(value - totalCalculated.value.calculatedTax) >= 0.005 ? value : null
|
||||
editingTotalTax.value = false
|
||||
}
|
||||
|
||||
// --- Saving ---
|
||||
const updateIncomingInvoice = async (setBooked = false) => {
|
||||
if (setBooked && hasBlockingIncomingInvoiceErrors.value) {
|
||||
const keepBooked = setBooked || isBookedIncomingInvoice.value
|
||||
|
||||
if (keepBooked && hasBlockingIncomingInvoiceErrors.value) {
|
||||
toast.add({
|
||||
title: "Buchen nicht möglich",
|
||||
description: "Bitte beheben Sie zuerst die rot markierten Pflichtfehler.",
|
||||
@@ -242,7 +293,7 @@ const updateIncomingInvoice = async (setBooked = false) => {
|
||||
let item = { ...itemInfo.value }
|
||||
item.accounts = (item.accounts || []).map((account) => ensureDepreciationDefaults({ ...account }, item.date))
|
||||
delete item.files
|
||||
item.state = setBooked ? "Gebucht" : "Entwurf"
|
||||
item.state = keepBooked ? "Gebucht" : "Entwurf"
|
||||
|
||||
await useEntities('incominginvoices').update(itemInfo.value.id, item, !setBooked)
|
||||
|
||||
@@ -303,8 +354,16 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
</h1>
|
||||
</template>
|
||||
<template #right>
|
||||
<UButton
|
||||
v-if="canEditIncomingInvoice"
|
||||
icon="i-heroicons-pencil-square"
|
||||
variant="outline"
|
||||
@click="navigateTo(`/incomingInvoices/edit/${route.params.id}`)"
|
||||
>
|
||||
Bearbeiten
|
||||
</UButton>
|
||||
<ArchiveButton
|
||||
v-if="mode !== 'show'"
|
||||
v-if="canArchiveIncomingInvoice"
|
||||
color="error"
|
||||
variant="outline"
|
||||
type="incominginvoices"
|
||||
@@ -314,7 +373,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
Speichern
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="mode !== 'show'"
|
||||
v-if="mode !== 'show' && !isBookedIncomingInvoice"
|
||||
@click="updateIncomingInvoice(true)"
|
||||
:disabled="hasBlockingIncomingInvoiceErrors"
|
||||
>
|
||||
@@ -872,7 +931,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
|
||||
<div class="col-span-6 md:col-span-3">
|
||||
<UFormField label="Steuerbetrag" help="Automatisch berechnet">
|
||||
<UInput class="w-full" :model-value="item.amountTax" disabled color="gray" >
|
||||
<UInput class="w-full" :model-value="item.amountTax" disabled color="gray">
|
||||
<template #trailing>€</template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
@@ -923,14 +982,34 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
<span>Netto Gesamt</span>
|
||||
<span>{{ totalCalculated.totalNet.toFixed(2) }} €</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-gray-500" v-if="totalCalculated.totalAmount7Tax > 0">
|
||||
<span>+ 7% USt</span>
|
||||
<span>{{ totalCalculated.totalAmount7Tax.toFixed(2) }} €</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-gray-500" v-if="totalCalculated.totalAmount19Tax > 0">
|
||||
<span>+ 19% USt</span>
|
||||
<span>{{ totalCalculated.totalAmount19Tax.toFixed(2) }} €</span>
|
||||
<div class="flex items-center justify-between gap-3 text-gray-500">
|
||||
<span class="flex items-center gap-2">
|
||||
USt gesamt
|
||||
<UBadge v-if="totalCalculated.hasTaxOverride" size="xs" color="warning" variant="soft">Manuell</UBadge>
|
||||
</span>
|
||||
<div v-if="editingTotalTax" class="flex items-center gap-1">
|
||||
<UInput v-model="totalTaxDraft" type="number" step="0.01" size="xs" class="w-28">
|
||||
<template #trailing>€</template>
|
||||
</UInput>
|
||||
<UButton icon="i-heroicons-check" size="xs" variant="ghost" @click="applyTotalTaxOverride" />
|
||||
<UButton icon="i-heroicons-x-mark" size="xs" color="neutral" variant="ghost" @click="editingTotalTax = false" />
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-1">
|
||||
<span>{{ totalCalculated.totalTax.toFixed(2) }} €</span>
|
||||
<UButton
|
||||
v-if="mode !== 'show'"
|
||||
icon="i-heroicons-pencil"
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
aria-label="Gesamten USt-Betrag bearbeiten"
|
||||
@click="startEditingTotalTax"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="totalCalculated.hasTaxOverride" class="text-xs text-amber-600 dark:text-amber-400">
|
||||
DATEV berechnet die USt aus dem Bruttobetrag erneut. Der Export wird mit „USt prüfen“ markiert.
|
||||
</p>
|
||||
<div class="flex justify-between font-bold text-xl text-gray-900 dark:text-white pt-2 border-t dark:border-gray-700">
|
||||
<span>Rechnungsbetrag</span>
|
||||
<span>{{ totalCalculated.totalGross.toFixed(2) }} €</span>
|
||||
|
||||
@@ -141,12 +141,7 @@ const displayCurrency = (value, currency = "€") => {
|
||||
return `${Number(value).toFixed(2).replace(".",",")} ${currency}`
|
||||
}
|
||||
const getInvoiceSum = (invoice) => {
|
||||
let sum = 0
|
||||
invoice.accounts.forEach(account => {
|
||||
sum += account.amountTax
|
||||
sum += account.amountNet
|
||||
})
|
||||
return sum.toFixed(2)
|
||||
return useSum().getIncomingInvoiceSum(invoice)
|
||||
}
|
||||
|
||||
const getPaidAmount = (item) => {
|
||||
|
||||
@@ -184,7 +184,7 @@ onMounted(load)
|
||||
|
||||
<UModal v-model:open="showForm" :title="editingId ? 'Importquelle bearbeiten' : 'Importquelle einrichten'">
|
||||
<template #body>
|
||||
<UForm class="space-y-5" @submit.prevent="save">
|
||||
<UForm :state="form" class="space-y-5" @submit.prevent="save">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<UFormField label="Bezeichnung" required>
|
||||
<UInput v-model="form.name" placeholder="Rechnungseingang" class="w-full" />
|
||||
|
||||
Reference in New Issue
Block a user