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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user